# Transaction splitting

Transaction splitting turns one incoming payment into several transactions, one for each `offer_id` of the checkout's products. You can then track, report, refund and pay out each product line separately. Typical uses:

- insurance companies (MTPL and CASCO products);
- marketplaces (different sellers);
- multi-brand retailers (different product lines);
- service providers (different service types).

## How it works

1. **One payment, several transactions.** A successful payment creates one transaction per distinct `offer_id`. Its amount is the sum of `unit_price × quantity` of the products with that `offer_id`.
2. **Tagged transactions.** Each transaction carries its `offer_id`, and the payment webhook lists them.
3. **One balance.** All the money goes to your Payout account, whatever the `offer_id`.
4. **Optional routing.** Split routing rules can pay out the transactions of an `offer_id` to a separate IBAN, see [Split routing rules](#split-routing-rules).

> [!NOTE]
> To have payouts routed by `offer_id`, send `should_split: true` even when the checkout has only one product or offer. Only then is the transaction tagged with the `offer_id` and routed by your rules.

## Enabling transaction splitting

Both conditions must be met:

1. **Account feature.** Ask support to enable transaction splitting for your account.
2. **Checkout flag.** Send `"should_split": true` (a JSON boolean, not the string `"true"`) when you [create the checkout](https://developers.payout.tech/api/payment.html#create_checkout).

## Products

Give every product an `offer_id`. As everywhere in the Payment API, `amount` and `unit_price` are integers in cents:

```json
{
  "amount": 30000,
  "currency": "EUR",
  "should_split": true,
  "products": [
    {
      "name": "Premium Service",
      "offer_id": "PREMIUM",
      "unit_price": 10000,
      "quantity": 1
    },
    {
      "name": "Basic Service",
      "offer_id": "BASIC",
      "unit_price": 20000,
      "quantity": 1
    }
  ]
}
```

This checkout pays 300.00 EUR: 100.00 EUR for `PREMIUM` and 200.00 EUR for `BASIC`. Products without an `offer_id` are grouped into one transaction with `offer_id` `null`, which cannot be refunded or routed by `offer_id`.

> [!IMPORTANT]
> With `should_split: true`, the sum of `unit_price × quantity` over all products **must equal the checkout `amount`**.

If a condition is not met, creating the checkout fails with HTTP `400`:

| Problem | `errors` |
| --- | --- |
| The sum of the products differs from `amount` | `[{"products": "Sum of products (250.00) must equal checkout amount (300.00)."}]` |
| No products | `[{"products": "Products must be provided when splitting is enabled."}]` |
| Splitting is not enabled for your account | `[{"shoud_split": "Transaction splitting not available."}]` |

Examples of `offer_id` values by industry:

- Insurance: `MTPL`, `CASCO`
- Marketplace: `VENDOR_A`, `VENDOR_B`
- Retail: `ELECTRONICS`, `CLOTHING`

## Payment webhook

The checkout webhooks of a split payment, such as `checkout.succeeded`, have a `split_transactions` list in `data`, next to `payment`:

```json
{
  "external_id": "order-1001",
  "object": "webhook",
  "type": "checkout.succeeded",
  "data": {
    "object": "checkout",
    "id": 141447,
    "external_id": "order-1001",
    "amount": 30000,
    "currency": "EUR",
    "status": "succeeded",
    "is_status_final": true,
    "payment": {
      "object": "payment",
      "status": "successful",
      "payment_method": "card",
      "fee": 250,
      "net": 9750
    },
    "split_transactions": [
      {
        "transaction_id": 9001,
        "amount": 10000,
        "offer_id": "PREMIUM",
        "bank_account": "SK3112000000198742637541"
      },
      {
        "transaction_id": 9002,
        "amount": 20000,
        "offer_id": "BASIC",
        "bank_account": null
      }
    ]
  },
  "nonce": "UzhER2lFOFZCNkNQVmNuNQ",
  "signature": "<signature>"
}
```

The example shows only the fields that matter here; the rest of the checkout object is the same as in other webhooks.

| Field | Description |
| --- | --- |
| `transaction_id` | ID of the split transaction |
| `amount` | Amount of the split transaction, in cents |
| `offer_id` | The `offer_id` of its products, or `null` for products without one |
| `bank_account` | IBAN of the active routing rule for this `offer_id`, or `null` when there is none |

`split_transactions` is present only when the checkout was created with `should_split: true` and has a successful payment. `fee` and `net` in `data.payment` refer to the first split transaction only.

## Split routing rules

Split routing rules say **where the payouts** of an `offer_id` go. They are applied by your scheduled automatic payout, so they need an automatic payout to be set up on your account. They do not affect incoming payments.

The rules **cannot be managed through the API or the portal**; our team sets them up for you.

### Setting up a rule

Contact **support** or your **account manager** with these details for each `offer_id`:

| Detail | Example | Notes |
| --- | --- | --- |
| `offer_id` | `PREMIUM` | Must match the `offer_id` of your products |
| IBAN | `SK3112000000198742637541` | Where the payouts of this `offer_id` go |
| Currency | `EUR` | Currency of the automatic payout the rule applies to |
| Description | `Premium Service Payouts` | Optional. Used as the beneficiary name; without it, your account name is used |

With each automatic payout, Payout sends the net amount of the available transactions of that `offer_id`, minus their refunds, to the rule's IBAN. The statement descriptor is `Split payout: <offer_id>`, and the usual withdrawal fee applies.

### Transactions without a rule

Transactions whose `offer_id` has no active rule stay in your general balance. They are paid out by your regular automatic payout or by a manual withdrawal. Any change to the rules goes through our support team.

## Refund handling

To [refund](https://developers.payout.tech/api/payment.html#refund_payment) a checkout created with `should_split: true`, you **must** send `offer_id`. It selects the split transaction to refund.

```http
POST /api/v1/refunds
Content-Type: application/json
Authorization: Bearer <token>
```

```json
{
  "checkout_id": 141447,
  "offer_id": "PREMIUM",
  "amount": 5000,
  "nonce": "<random string>",
  "signature": "<signature>"
}
```

- `amount` is optional, in cents. Without it, the whole remaining amount of the split transaction is refunded. Card payments processed by CardPay and pay-later payments can only be refunded in full.
- `signature` is the SHA-256 of `amount|currency|external_id|iban|nonce|client_secret`, encoded as lowercase hex. `currency` and `external_id` are those of the checkout, and `iban` is empty unless you send one. If you leave out `amount`, sign the **full checkout amount**, not the remaining amount of the split transaction.

| Problem | HTTP status | Body |
| --- | --- | --- |
| Invalid `signature` | `403` | `{"errors": {"signature": "Is invalid"}}` |
| `offer_id` missing | `400` | `{"errors": {"offer_id": "is required for split refund"}}` |
| No split transaction with this `offer_id` | `422` | `{"errors": "split_transaction_not_found"}` |

The response is the usual refund object; it does not repeat the `offer_id`:

```json
{
  "id": 5821,
  "object": "refund",
  "amount": 5000,
  "currency": "EUR",
  "external_id": "order-1001",
  "status": "pending",
  "metadata": {},
  "created_at": 1759744800
}
```

## Good to know

- Splitting works for payment methods that pay the checkout directly, such as cards, online banking and pay later. Payments by bank transfer with [payment instructions](https://developers.payout.tech/guides/payment-gateway-use-cases-payment-instructions.html), which are matched by their reference, are not split.
- Fees are calculated for each split transaction separately: each one is charged the fixed fee and the percentage fee of its own amount.
- Each split transaction stays linked to the original payment; the webhook gives you its `transaction_id` and `offer_id`.
- Split routing rules only affect payouts, not incoming payments.
