Create transfer batch
Changed onExecutes an outbound transfer batch from one exact SDP Wallet ID, chunks recipients into Solana transactions, and returns the batch, recipient, and transfer records. Requires an Idempotency-Key: a retry with the same key and request returns the original batch without another on-chain submission. Recipient order does not change the request. A native SOL batch is refused with 409 AMOUNT_BELOW_RENT_MINIMUM, before anything is written or sent, when a recipient whose address has no account gets less than the rent-exempt minimum (details.recipientIndexes) or the source wallet would be left holding more than nothing but less than the minimum (details.sourceRemainder). A token batch is refused with 409 SPONSORED_RENT_OVER_CAP when one recipient's new token account costs more rent than the sponsor funds in a transaction (details.recipientIndex). An externalId already used by a batch on the same project and source wallet never pays again: the same request from the same credential returns that batch, and any other returns 409 TRANSFER_BATCH_RUN_EXISTS with its batchId in error.details. To finish a batch after a partial failure, call POST /v1/payments/transfer-batches/{batchId}/retry: it sends only the batch's failed recipients. Never resend the whole batch under a new Idempotency-Key, which pays the recipients that succeeded again.
Request
- Base URLhttp://localhost:8787
- URLhttp://localhost:8787/v1/payments/transfer-batches
- AuthHTTP bearer
Headers
Required for dashboard sessions. API keys use their assigned project.
Required: a request without one returns 400 IDEMPOTENCY_KEY_REQUIRED. 1-255 printable ASCII characters; send a UUID per logical operation and reuse it on every retry. Within 24 hours, a retry with the same key and the same request returns the stored status and body with the response header Idempotent-Replayed: true. Keys are scoped to the credential: within 24 hours, the same key with a different request, or from a different credential, returns 422 IDEMPOTENCY_KEY_REUSED; after that, the same key from a different credential runs as a new request. A rotated API key is a new credential, so after any rotation read the resource with GET to see whether the original request ran, and never resend a money request under the new credential without checking first. A key whose original request is still running returns 409 IDEMPOTENCY_KEY_IN_FLIGHT with Retry-After. A 400, 401, 403, 409 or 429 refusal is not stored, so the same key runs again once the cause is fixed. Access is checked before the key on every request, so a refused request never claims the key, and a credential that has since lost access to the named resource gets 403 instead of the stored response. A replay is a snapshot of the original response: read the current state with GET instead of sending the request again. If the original request never recorded its outcome (it crashed), a same-key retry gets 409 IDEMPOTENCY_KEY_IN_FLIGHT until 15 minutes plus the route's lease have passed since the original was sent (about 16 minutes, or about 20 for transfer batches), then takes over and is answered from the record it wrote. Once the request has written its record, any answer below 500 is stored: a transfer that fails after that (such as 400 TRANSACTION_FAILED or a 409 signer mismatch) answers a same-key retry with the same error and Idempotent-Replayed: true, and after 24 hours the record answers with the same status and code. A 5xx is not stored: a same-key retry runs again and is answered from the record. To try again after a failure, send the request with a new key. After a 5xx, a retry with the same key runs again and answers from the original record instead of acting twice; that answer also carries Idempotent-Replayed: true.
Request body
Example request
{
"projectId": "prj_example",
"sourceCustodyWalletId": "cwlt_example",
"token": "SOL",
"recipients": [
{
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"amount": "25.00"
}
],
"options": {
"maxRecipientsPerTransaction": 20,
"priorityFee": "auto",
"preflight": true
},
"externalId": "payroll_2026_06_30"
}Response
Transfer batch created
Example response
{
"data": {
"batch": {
"id": "xbatch_example",
"organizationId": "org_example",
"projectId": "prj_example",
"externalId": "payroll_2026_06_30",
"sourceCustodyWalletId": "cwlt_example",
"sourceProviderWalletId": "privy_wallet_123",
"sourceAddress": "So11111111111111111111111111111111111111112",
"token": "SOL",
"status": "processing",
"totalAmount": "1000.00",
"recipientCount": 20,
"transactionCount": 1,
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
},
"recipients": [
{
"transferId": "xfr_example",
"externalId": "payroll_row_001",
"counterpartyId": "cpty_example",
"counterpartyAccountId": "cpacct_example",
"destination": "So11111111111111111111111111111111111111112",
"amount": "100.00",
"status": "pending",
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
],
"transfers": [
{
"id": "xfr_example",
"organizationId": "org_example",
"custodyWalletId": "cwlt_example",
"providerWalletId": "privy_wallet_123",
"projectId": "prj_example",
"type": "transfer",
"direction": "outbound",
"status": "confirmed",
"signature": "sig_example",
"serializedTx": "base64_tx_example",
"slot": 123456,
"blockTime": "2025-01-01T00:00:00.000Z",
"confirmedAt": "2025-01-01T00:00:00.000Z",
"fee": 5000,
"error": "Signature failed",
"initiatedBy": {
"type": "api_key"
},
"source": "So11111111111111111111111111111111111111112",
"destination": "So11111111111111111111111111111111111111112",
"rampsMemo": {
"invoice": "INV-123",
"po": "PO-9"
},
"amount": "100.00",
"counterpartyId": "cpty_example",
"counterpartyDisplayName": "Acme Studio",
"providerReference": "ramp_quote_example",
"deliveryMode": "session_widget",
"fiatCurrency": "USD",
"fiatAmount": "100.00",
"fiatPayout": {
"amount": "20.82",
"currency": "USD",
"reportedAt": "2025-01-02T00:00:00.000Z"
},
"deliveryMismatch": {
"reason": "amount_mismatch",
"checkedAt": "2025-01-02T00:00:00.000Z",
"reportedAmount": "20",
"observedAmount": "0.5"
},
"risk": {
"provider": "trm",
"score": "0.12",
"level": "low",
"evaluatedAt": "2025-01-01T00:00:00.000Z"
},
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-02T00:00:00.000Z"
}
]
},
"meta": {
"requestId": "req_example",
"timestamp": "2025-01-01T00:00:00.000Z"
}
}Changes
- warning
added the new
CUSTODY_POLICY_REFUSEDenum value to the/response property for the response status - warning
added the new
CUSTODY_POLICY_REFUSEDenum value to the/response property for the response status - warning
added the new
CUSTODY_POLICY_REFUSEDenum value to the/response property for the response status - warning
added the new
CUSTODY_POLICY_REFUSEDenum value to the/response property for the response status - warning
added the new
CUSTODY_POLICY_REFUSEDenum value to the/response property for the response status - warning
added the new
CUSTODY_POLICY_REFUSEDenum value to the/response property for the response status - warning
added the new
CUSTODY_POLICY_REFUSEDenum value to the/response property for the response status
- warning
- breaking
the request property
became required - warning
added the new
RPC_UNAVAILABLEenum value to the/response property for the response status - warning
added the new
RPC_UNAVAILABLEenum value to the/response property for the response status - warning
added the new
RPC_UNAVAILABLEenum value to the/response property for the response status - warning
added the new
RPC_UNAVAILABLEenum value to the/response property for the response status - warning
added the new
RPC_UNAVAILABLEenum value to the/response property for the response status - warning
added the new
RPC_UNAVAILABLEenum value to the/response property for the response status - warning
added the new
RPC_UNAVAILABLEenum value to the/response property for the response status - warning
added the new
SPONSORED_RENT_OVER_CAPenum value to the/response property for the response status - warning
added the new
SPONSORED_RENT_OVER_CAPenum value to the/response property for the response status - warning
added the new
SPONSORED_RENT_OVER_CAPenum value to the/response property for the response status - warning
added the new
SPONSORED_RENT_OVER_CAPenum value to the/response property for the response status - warning
added the new
SPONSORED_RENT_OVER_CAPenum value to the/response property for the response status - warning
added the new
SPONSORED_RENT_OVER_CAPenum value to the/response property for the response status - warning
added the new
SPONSORED_RENT_OVER_CAPenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_DESTINATION_CHANGEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_DESTINATION_CHANGEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_DESTINATION_CHANGEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_DESTINATION_CHANGEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_DESTINATION_CHANGEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_DESTINATION_CHANGEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_DESTINATION_CHANGEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_NOTHING_TO_RETRYenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_NOTHING_TO_RETRYenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_NOTHING_TO_RETRYenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_NOTHING_TO_RETRYenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_NOTHING_TO_RETRYenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_NOTHING_TO_RETRYenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_NOTHING_TO_RETRYenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RETRY_CLAIMEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RETRY_CLAIMEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RETRY_CLAIMEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RETRY_CLAIMEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RETRY_CLAIMEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RETRY_CLAIMEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RETRY_CLAIMEDenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RUN_EXISTSenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RUN_EXISTSenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RUN_EXISTSenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RUN_EXISTSenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RUN_EXISTSenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RUN_EXISTSenum value to the/response property for the response status - warning
added the new
TRANSFER_BATCH_RUN_EXISTSenum value to the/response property for the response status
- breaking