Implementation Guide

Build your first payout

Send 1,500,000 COP from a wallet to a beneficiary and confirm the final outcome.

10 min to implement · Verified against the v4 API on 2026-10-11

The rule to remember: generate the idempotency key before you send the request, store it, and reuse it for every retry of the same payout. Then wait for completed, failed or cancelled. Anything else is not an outcome yet.

The job

A marketplace owes a seller 1,500,000 COP. The money sits in the seller's wallet on Orangepill, and the seller has a Bre-B key at their bank. You want the money to arrive once, and you want to be able to show what happened.

Before you start

  • An API key for the sandbox. Sandbox access is provided during evaluation.
  • A wallet account in COP with enough available balance.
  • A payout route for the product you use (here bank_transfer.bre_b) configured for your tenant.
  • Somewhere in your own database to store the idempotency key and the returned payoutId.

Flow

  1. Request idempotency_key checked
  2. Reserve Reservation recorded
  3. Attempt Provider executes
  4. Outcome completed / failed
  5. Ledger Journal posted on completion

Implementation

1. Create the idempotency key first

Derive it from your own record, for example the settlement line you are paying, and save it before calling the API. If your process crashes after sending the request, you still know which key you used.

2. Create the payout

Send exactly one of product_key (Orangepill picks the provider from your routes) or providerInstanceId (you name the provider). Amounts are strings in minor units, so 1,500,000.00 COP is "150000000".

Create a payout
curl -X POST "$ORANGEPILL_API/v4/payouts" \
  -H "Authorization: Bearer $ORANGEPILL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "payout-7f3c2a10",
    "product_key": "bank_transfer.bre_b",
    "amount": { "value": "150000000", "currency": "COP" },
    "recipient": { "scheme": "ALIAS", "value": "<beneficiary Bre-B key>" },
    "source": { "type": "wallet", "wallet_account_id": "<wallet account id>" },
    "metadata": { "order_ref": "settlement-2026-10-11-0042" }
  }'
Response
{
  "success": true,
  "data": {
    "payoutId": "8d1e5c3a-…",
    "status": "pending",
    "reservationId": "c2b7f9e4-…"
  }
}

Store payoutId next to your key. reservationId is the reservation recorded against the wallet for this payout. Today that reservation does not reduce the available balance reported by the balance endpoint, and creating the payout does not check the balance, so check available balance yourself before you create a wallet-funded payout.

3. Follow the payout to a terminal status

There are no outbound webhooks for payouts today, so read the payout until it is final. pending and processing mean the provider has not given a final answer.

Read the payout
curl "$ORANGEPILL_API/v4/payouts/$PAYOUT_ID" \
  -H "Authorization: Bearer $ORANGEPILL_API_KEY"
Response
{
  "success": true,
  "data": {
    "id": "8d1e5c3a-…",
    "idempotency_key": "payout-7f3c2a10",
    "status": "completed",
    "amount": { "value": "150000000", "currency": "COP" },
    "providerPayoutId": "…",
    "errorCode": null,
    "errorMessage": null,
    "createdAt": "2026-10-11T14:02:11Z",
    "completedAt": "2026-10-11T14:02:48Z"
  }
}
Polling sketch (JavaScript)
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);

async function waitForOutcome(payoutId) {
  for (let delay = 2000; ; delay = Math.min(delay * 2, 60000)) {
    const res = await fetch(`${API}/v4/payouts/${payoutId}`, { headers: auth });
    const { data } = await res.json();
    if (TERMINAL.has(data.status)) return data;   // final: act on it
    await sleep(delay);                           // pending / processing: not final yet
  }
}

4. Check the ledger effect

When a wallet-funded payout completes, the reservation is consumed and a journal is posted that debits the wallet. If it fails, the reservation is released and nothing is posted.

Ledger entries for this payout
curl "$ORANGEPILL_API/v4/ledger/entries/by-reference?referenceType=payout&referenceId=$PAYOUT_ID" \
  -H "Authorization: Bearer $ORANGEPILL_API_KEY"

For a graph of how an entity reached its state, GET /v4/financial/explain accepts a subjectType (such as ledger_journal) and a subjectId.

What can go wrong

  • Your request times out. The payout may exist. Send the same request with the same idempotency_key: you get the existing payout back, not a new one. The status in that response is currently always pending, so read GET /v4/payouts/{payoutId} for the real state. See Handle a payout timeout safely.
  • Neither or both of product_key and providerInstanceId. The request is rejected with 400.
  • The provider fails the payout. Status becomes failed with an errorCode such as PROVIDER_FAILURE. The reservation is released.
  • failed with RETRY_EXHAUSTED. Orangepill stopped checking the provider without receiving a final answer. Treat it as unknown, not as a decline: do not pay again until the provider outcome is established.
  • The payout stays non-terminal for a long time. See the reservation note below and escalate.

What Orangepill guarantees today

  • One payout per idempotency_key in your tenant. A repeated key returns the existing payout.
  • On completion of a wallet-funded payout, its reservation is consumed and a journal referencing the payout is posted. On failure the reservation is released and nothing is posted.

What Orangepill does not guarantee

  • A payout that fails on one provider is not retried on another. Payout failover is in development.
  • A balance check at payout creation. The payout's reservation does not currently reduce available balance; check it yourself first.
  • Reservations are not held indefinitely. A payout reservation currently expires after one hour, even if the payout is still pending or processing.
  • Exactly-once delivery across external providers. The key prevents duplicate payouts in Orangepill; the rail behaves as the rail behaves.

Production considerations

  • Production uses its own API key and its own provider configuration. Nothing carries over from sandbox.
  • Persist the idempotency key, the payoutId and the last status you saw.
  • Alert on payouts that are still non-terminal after the time you expect for that rail, and well before one hour.
  • Test a client retry after a timeout and a failed payout in sandbox before launch, and check available balance in your own code before creating a payout.
  • Your operations team should be able to open a payout and see its status, error code and ledger entries.

The full list is in the production checklist.

Build this with us.

Sandbox access and API credentials are provided during evaluation.