> For the complete documentation index, see [llms.txt](https://docs.violet.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.violet.io/api-reference/orders-and-checkout/bag-transaction-fees.md).

# Bag Transaction Fees

Submit app-provided transaction fees at the bag level.

### Overview

A channel submits a transaction fee for a single bag. Violet validates the submission (bag exists, bag is owned by your app, amounts and currency are valid, configuration permits app-provided fees), persists the fee, and returns the outcome. The fee value you send is treated as authoritative; Violet does not recompute it.

{% hint style="warning" %}
**Preview.** The submission, validation, and persistence path is live. The fee does not yet affect distributions — that consumption behavior ships in a later release.
{% endhint %}

### Request body

| Field                               | Type    | Required         | Description                                                                                                                     |
| ----------------------------------- | ------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `transaction_fee_total_amount`      | integer | Yes              | The total fee, in minor units (e.g. cents). **Authoritative** — Violet uses this value as-is. Must be ≥ 0.                      |
| `transaction_fee_percentage_amount` | integer | Yes              | The portion of the fee attributable to your percentage component, in minor units. Submit `0` if it does not apply. Must be ≥ 0. |
| `transaction_fee_fixed_amount`      | integer | Yes              | The portion of the fee attributable to your fixed component, in minor units. Submit `0` if it does not apply. Must be ≥ 0.      |
| `currency`                          | string  | Yes              | ISO 4217 currency code. Must match the bag's base currency. Compared case-insensitively.                                        |
| `source_type`                       | string  | Yes              | Must be `APP_PROVIDED`. Any other value is rejected.                                                                            |
| `source_provider`                   | string  | No               | Optional free-form identifier for the source of the fee.                                                                        |
| `source_reference_id`               | string  | No               | Optional. Your own reference ID for this fee, for reconciliation.                                                               |
| `idempotency_key`                   | string  | No (recommended) | Safe-retry key. See Idempotency below.                                                                                          |

{% hint style="info" %}
All three amount fields are required and must be present, even when a component is `0`. Amounts are integer minor units; do not send decimals. Violet does not recalculate the percentage/fixed components against the bag total — send your resolved values.
{% endhint %}

The submitting app is identified by the `X-Violet-App-Id` header and must own the bag. A submission for a bag owned by a different app is rejected with HTTP 403.

### Example request

```bash
curl -X PUT "https://api.violet.io/v1/bags/1234567/transaction_fee" \
  -H "X-Violet-Token: <token>" \
  -H "X-Violet-App-Secret: <app-secret>" \
  -H "X-Violet-App-Id: 10355" \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_fee_total_amount": 390,
    "transaction_fee_percentage_amount": 290,
    "transaction_fee_fixed_amount": 100,
    "currency": "USD",
    "source_type": "APP_PROVIDED",
    "source_provider": "acme",
    "source_reference_id": "acme-fee-8842",
    "idempotency_key": "bag-1234567-fee-v1"
  }'
```

### Successful response (HTTP 200)

```json
{
  "outcome": "accepted",
  "error_code": null,
  "bag_transaction_fee": {
    "id": 10001,
    "bag_id": 1234567,
    "order_id": 987654,
    "app_id": 10355,
    "merchant_id": 4421,
    "currency": "USD",
    "transaction_fee_total_amount": 390,
    "transaction_fee_percentage_amount": 290,
    "transaction_fee_fixed_amount": 100,
    "source_type": "APP_PROVIDED",
    "source_app_id": 10355,
    "source_provider": "acme",
    "source_reference_id": "acme-fee-8842",
    "status": "ACCEPTED",
    "idempotency_key": "bag-1234567-fee-v1",
    "config_mode": "APP_PROVIDED",
    "received_at": "2026-07-06T21:30:00+0000",
    "accepted_at": "2026-07-06T21:30:00+0000",
    "created_by": "app:10355",
    "date_created": "2026-07-06T21:30:00+0000",
    "date_last_modified": "2026-07-06T21:30:00+0000"
  }
}
```

The `outcome` field indicates the result of the submission:

| `outcome`                  | Meaning                                                                                                                                                                                                |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `accepted`                 | The fee was validated and stored.                                                                                                                                                                      |
| `duplicate_accepted`       | An identical submission under the same `idempotency_key` was already accepted; no new record was created.                                                                                              |
| `config_conflict_rejected` | Your app is not currently permitted to supply app-provided fees for this bag's merchant. Returned as **HTTP 200** with `error_code = CONFIG_DISALLOWS_APP_PROVIDED_FEE` and a stored rejection record. |

{% hint style="warning" %}
A configuration conflict is returned as a **200** with `outcome = config_conflict_rejected`, not as a 4xx. Validation failures (below) are 4xx and use the standard error envelope.
{% endhint %}

### Error responses (HTTP 4xx)

Validation failures use Violet's standard error envelope:

```json
{
  "message": "Bag identified by 1234567 could not be found.",
  "error": "bag_not_found",
  "code": 4201
}
```

| `error`                    | HTTP | `code` | When                                                                   |
| -------------------------- | ---- | ------ | ---------------------------------------------------------------------- |
| `bag_not_found`            | 404  | 4201   | The `bag_id` does not resolve to a bag.                                |
| `insufficient_permissions` | 403  | 9901   | The bag is not owned by the submitting app.                            |
| `missing_parameter`        | 400  | 9002   | A required field is missing (an amount, `currency`, or `source_type`). |
| `invalid_currency`         | 400  | 4226   | `currency` does not match the bag's base currency.                     |
| `invalid_amount`           | 400  | 4465   | An amount is negative.                                                 |
| `invalid_source_type`      | 400  | 4466   | `source_type` is not `APP_PROVIDED`.                                   |
| `duplicate_conflict`       | 409  | 4467   | The `idempotency_key` was reused with different values.                |

### Idempotency

`idempotency_key` is optional but recommended for safe retries.

* Resubmitting the **same** values under the same key returns `duplicate_accepted` and does not create a second record.
* Reusing the same key with **different** values returns `duplicate_conflict` (HTTP 409).
* Without a key, each submission is processed independently (no de-duplication).

### Validation order

Violet validates in this order and returns the first failure:

1. Bag exists (`bag_not_found`).
2. Bag owned by the submitting app (`insufficient_permissions`).
3. All three amounts present (`missing_parameter`).
4. Amounts non-negative (`invalid_amount`).
5. `currency` present and matches the bag's base currency (`missing_parameter` / `invalid_currency`).
6. `source_type` present and equal to `APP_PROVIDED` (`missing_parameter` / `invalid_source_type`).
7. Idempotency check (`duplicate_accepted` / `duplicate_conflict`).
8. Configuration gate (`config_conflict_rejected` if not permitted).
9. Accept and persist (`accepted`).

**Available endpoints:**

```
PUT /v1/bags/{bag_id}/transaction_fee
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.violet.io/api-reference/orders-and-checkout/bag-transaction-fees.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
