> 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="info" %}
An accepted fee is applied to the bag's split only when the retailer's fee settings use it: the fee is deducted before commission, the retailer bears it, or both. For every other retailer the fee is stored and changes nothing. See [Net Receipts](/prism/payments/payouts/net-receipts.md).
{% endhint %}

### When to submit

Submit the fee as soon as you know it, ideally before the order is accepted. What happens when it arrives later depends on the retailer's configuration:

* **Retailers whose fee settings use the fee.** If no accepted fee exists when Violet generates the bag's distributions, Violet holds the bag's payout until the fee arrives. When it arrives, the split is computed with it and the retailer's share follows its usual transfer timing. After 72 hours with no fee, the hold is marked `MISSING_FEE` and Violet follows up with you, but the bag keeps waiting: a fee that arrives later is still accepted and releases it. Violet never applies a default fee. A fee that arrives after the bag's payout has been computed without waiting for it (for example, an order accepted before the retailer was configured for Net Receipts) is not applied: the response is `late_rejected`.
* **All other retailers.** The fee does not change the split, so nothing waits for it. A fee submitted at any time is `accepted`.

### 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.                                                                                                                                                                                                       |
| `late_rejected`            | The retailer is configured for Net Receipts and the bag's split no longer waits for a fee, so this fee was not applied. Returned as **HTTP 200** with a stored record whose `status` is `LATE_REJECTED`. `error_code` is `FEE_ALREADY_APPLIED` when the split already carries a fee (a second fee for the same bag), or `SUBMISSION_TOO_LATE` when the split was computed without one. Nothing else changes. |

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

A `late_rejected` record carries no `idempotency_key`, so resubmitting the same values returns `late_rejected` again rather than `duplicate_accepted`.

```json
{
  "outcome": "late_rejected",
  "error_code": "FEE_ALREADY_APPLIED",
  "bag_transaction_fee": {
    "id": 10043,
    "bag_id": 1234567,
    "order_id": 987654,
    "app_id": 10355,
    "merchant_id": 4421,
    "currency": "USD",
    "transaction_fee_total_amount": 410,
    "transaction_fee_percentage_amount": 310,
    "transaction_fee_fixed_amount": 100,
    "source_type": "APP_PROVIDED",
    "source_app_id": 10355,
    "source_provider": "acme",
    "source_reference_id": "acme-fee-8843",
    "status": "LATE_REJECTED",
    "config_mode": "APP_PROVIDED",
    "rejection_reason": "FEE_ALREADY_APPLIED",
    "received_at": "2026-07-06T22:10:00+0000",
    "created_by": "app:10355",
    "date_created": "2026-07-06T22:10:00+0000",
    "date_last_modified": "2026-07-06T22:10:00+0000"
  }
}
```

### 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. Lateness, for retailers configured for Net Receipts (`late_rejected` if the bag's split no longer waits for a fee).
10. 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 by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

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.
