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:
- Resolve recipients in your application — list (or stream) the rows to be paid, with destination address and amount per row.
- Pick a batch identifier you control (
payout_2026-05-14_marketplace_sellers). - Iterate — for each recipient, call
POST /v1/payments/transferswith 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). - Capture the returned
Transfer.idandTransfer.status. Persist the SDP transfer ID alongside the recipient row so you can re-poll status later. - 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 matchingfinalizedtransfer.
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:
- Idempotency keys — generate one stable
Idempotency-Keyper 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 returns409. - 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.
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:
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). Theerrorfield on the transfer record explains what reached the chain. The recipient was not paid; you can retry by submitting a new transfer.pendingpast the expected confirmation window — usually a backend or RPC hiccup. Re-poll withGET /v1/payments/transfers/{id}rather than re-submitting; you only want a new transfer ifstatussettles tofailed.- Mid-run worker crash — resume with each row's original
Idempotency-Key; persist the returnedTransfer.idbefore 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.
Related
- Basic payment — the underlying transfer endpoint.
- Payment with memo — the memo field used above to tag a batch.
- Indexing and reconciliation — the same list endpoint, used inbound.
- Wallet policies — destination allowlist and daily-limit guardrails for payout wallets.