Payouts and disbursements

Run batched outbound transfers, deduplicate retries, and reconcile a payout batch with SDP.

A payout run is a batch of outbound transfers — marketplace seller payouts, payroll, affiliate disbursements, royalty distributions. Use POST /v1/payments/transfer-batches when recipients are registered counterparty accounts. For arbitrary Solana addresses, call POST /v1/payments/transfers once per recipient and reconcile the returned transfer IDs.

Native transfer batches

One request creates a durable batch, chunks its recipients into Solana transactions, and returns the batch, recipient, and transfer records:

curl -X POST https://api.solana.com/v1/payments/transfer-batches \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payroll-2026-05-14" \
  -d '{
    "externalId": "payroll_2026-05-14",
    "sourceCustodyWalletId": "cwlt_123",
    "token": "9aBCd...4eEf",
    "recipients": [
      {
        "externalId": "row_4837",
        "counterpartyId": "cp_4837",
        "counterpartyAccountId": "cpa_4837",
        "amount": "150.00"
      }
    ]
  }'

Reuse the same Idempotency-Key for retries of that exact request. Use GET /v1/payments/transfer-batches/{batchId} to inspect recipient and chunk outcomes. externalId is a correlation field, not an idempotency key.

The rest of this page covers the per-recipient alternative for destinations that are not registered counterparty accounts.

Designing a payout run

A typical pattern:

  1. Resolve recipients in your application — list (or stream) the rows to be paid, with destination address and amount per row.
  2. Pick a batch identifier you control (payout_2026-05-14_marketplace_sellers).
  3. Iterate — for each recipient, call POST /v1/payments/transfers with the destination, amount, token, and a memo that encodes the batch ID and a per-row identifier (payout_2026-05-14_marketplace_sellers/row_4837).
  4. Capture the returned Transfer.id and Transfer.status. Persist the SDP transfer ID alongside the recipient row so you can re-poll status later.
  5. Reconcile — once the batch is fully submitted, run GET /v1/payments/transfers?direction=outbound&from=…&to=… and confirm every row in your application has a matching finalized transfer.

Send transfers sequentially or in modest concurrency (4–8 in flight); high concurrency increases the chance of priority-fee contention and blockhash expiry without much throughput gain.

Dedup and retries

Two layers protect you against double-spends if a worker dies mid-run:

  1. Idempotency keys — generate one stable Idempotency-Key per recipient row and reuse it for retries. An identical request for the same exact source wallet returns the original transfer; reusing the key for a different request returns 409.
  2. Application mapping — persist the SDP transfer ID for each recipient row before moving to the next row. On restart, replay with the same idempotency key or skip rows that already have an associated Transfer.id.

The on-chain signature is unique across SDP transfers once populated. Use it as a cross-indexer integrity check, not as the retry mechanism.

Using a fresh idempotency key creates a new transfer intent, even if the payload is otherwise identical.

Terminal
curl -X POST https://api.solana.com/v1/payments/transfers \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: payout-row-4837" \
  -d '{
    "sourceCustodyWalletId": "cwlt_123",
    "destination": "7xKXz...9fGh",
    "token": "9aBCd...4eEf",
    "amount": "150.00",
    "memo": "payout_2026-05-14_marketplace_sellers/row_4837"
  }'

Reconciling a run

After the loop finishes, list all outbound transfers in the run's time window and tally against your recipient table:

Terminal
curl "https://api.solana.com/v1/payments/transfers?direction=outbound&from=2026-05-14T00:00:00Z&to=2026-05-14T23:59:59Z&pageSize=100" \
  -H "Authorization: Bearer sk_test_..."

Filter the returned transfers on memo.startsWith(batchId) (or whatever convention you chose) to scope to the batch. Cross-reference each recipient row by SDP Transfer.id.

Failure modes

Per-transfer outcomes you should expect to handle:

  • failed — the transaction was built and submitted but the network rejected it (insufficient funds, frozen account, expired blockhash). The error field on the transfer record explains what reached the chain. The recipient was not paid; you can retry by submitting a new transfer.
  • pending past the expected confirmation window — usually a backend or RPC hiccup. Re-poll with GET /v1/payments/transfers/{id} rather than re-submitting; you only want a new transfer if status settles to failed.
  • Mid-run worker crash — resume with each row's original Idempotency-Key; persist the returned Transfer.id before advancing.
  • Source wallet runs out of SOL for fees — submissions start failing with fee-related errors. If SDP is configured to sponsor fees this won't happen; otherwise top up the source wallet's SOL balance before continuing.
Is this page helpful?