Skip to Content
API integrationPayouts API

Payouts API

A payout sends a token amount from the project’s hot wallet to an address you name: a refund, an affiliate payment, a customer withdrawal. It is signed on your server with the hot wallet key that only your server holds, and broadcast by the payout worker. See Payouts for the dashboard side and Hot wallet for setup.

Three endpoints take the API-Key header:

MethodPathPurpose
POST/api/v1/withdrawal/merchantCreate a payout request.
GET/api/v1/withdrawal/{id}/merchantRead one payout.
GET/api/v1/withdrawal/merchantList payouts with filters and pagination.

Not available through the API in this release: the address book (saved recipients), approving or rejecting a held payout, cancelling a payout, and editing limits. These are dashboard operations. Amounts are in token units; convert from USD with GET /api/v1/ticker.

Lifecycle

statusMeaning
pending-otp-verificationDashboard requests only: waiting for the requester’s email code.
pending-approvalWaiting for an installation administrator to approve in the dashboard (email code). API requests land here unless automatic approval applies.
pendingApproved; waiting for the worker.
initiatedThe signed transaction was broadcast, or a broadcast was attempted with an uncertain result. txHash is set.
sentThe transaction is confirmed on chain with a matching transfer. fee is known.
processedFinal bookkeeping done after sent. Terminal.
failedThe transaction failed on chain or could not be sent. failureReason is set. Terminal.
rejectedAn administrator rejected it. Terminal.
cancelledCancelled from the dashboard before a transaction hash existed. Terminal.

Normal path: pending-approval → pending → initiated → sent → processed. Treat payout.sent as the success event; payout.processed follows shortly after.

Automatic approval: when the project has automatic approval enabled and the payout’s USD value is at or below the project’s automatic approval maximum, an API request is created as pending with an approval time and no approver, and goes straight to the worker. Otherwise it is pending-approval with approvalReason above_auto_approve or auto_approve_disabled. Automatic approval never overrides the minimum, the recipient limits or the balance check.

POST /api/v1/withdrawal/merchant

Headers: API-Key, Content-Type: application/json, optional Idempotency-Key.

curl -X POST https://<your-site>/api/v1/withdrawal/merchant \ -H "API-Key: 811b12035f0dfa8ffd62296df3c98b27" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: refund-order-8812" \ -d '{ "email": "[email protected]", "blockchainCode": "SOLANA", "currencyCode": "USDC", "amount": "100", "toAddress": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "customerID": "1234" }'
FieldTypeRequiredConstraints
emailstringyesRecipient email, at most 320 characters. Stored lowercased on the payout; it does not change the customer’s profile email.
blockchainCodestringyesSOLANA or ROBINHOOD. The chain must be active on this install’s network.
currencyCodestringyesCurrency code (^[A-Z0-9]{1,16}$). Must be active in the catalog and enabled for the project.
amountstringyesDecimal string in token units, not USD: at most 22 integer and 18 fractional digits, positive, and representable in the currency’s decimals. Numbers are rejected.
toAddressstringyesDestination address, at most 128 characters. Must differ from the hot wallet’s address.
customerIDstringyesYour identifier for the recipient. A missing customer is created; an inactive one is rejected.
idempotencyKeystringnoSame meaning as the header; the header wins when both are given.

Idempotency-Key: 1 to 128 printable non-space ASCII characters, scoped to the project. A replay with the same key and the same email, address, chain, currency, amount and customer returns 201 with the payout’s current record, even if the currency has since been disabled. The same key with different terms is 409 PAYOUT_DUPLICATE_REQUEST. Without a key, a request whose project, address, amount, currency and chain match a payout created within payout.duplicateWindowSeconds (default 300; 1 to 86400) by the API or the dashboard is 409 PAYOUT_DUPLICATE_REQUEST.

Checks, in USD at a fresh price, per recipient email per project: the minimum (payout.minimumUsd, default 1, or the project’s own minimum), the hourly and daily limits, and the automatic approval maximum. The token amount plus every non-terminal payout from the same hot wallet must fit the wallet’s current balance.

201 Created

{ "id": 120, "createdAt": "2026-06-19T13:14:41.806Z", "updatedAt": "2026-06-19T13:14:41.806Z", "blockchainCode": "SOLANA", "currencyCode": "USDC", "currencyType": "token", "amount": "100", "priceInUSD": "1", "amountInUSD": "100.000000", "toAddress": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "tokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "recipientEmail": "[email protected]", "txHash": null, "status": "pending-approval", "type": "payout_merchant", "attributes": "{\"approvalReason\":\"above_auto_approve\"}", "createdBy": "api", "fromAddress": null, "failureReason": null, "webhookStatus": "pending", "fee": null }
FieldTypeMeaning
idintegerPayout id, used in the read path and in webhooks as payout_id.
createdAt, updatedAtstringISO 8601.
blockchainCodestringSOLANA or ROBINHOOD.
currencyCodestringCurrency code, frozen at creation.
currencyTypestringnative or token.
amountstringToken amount as requested.
priceInUSDstringUSD price of one token at creation.
amountInUSDstringUSD value at creation, six decimals.
toAddressstringDestination.
tokenAddressstringMint or contract; native for SOL, the zero address for ETH.
recipientEmailstringThe email given.
txHashstring or nullSet from initiated on.
statusstringOne of the nine statuses above.
typestringAlways payout_merchant.
attributesstringA JSON string. {"approvalReason":"above_auto_approve"}, {"approvalReason":"auto_approve_disabled"} or {}.
createdBystringapi or dashboard.
fromAddressstring or nullThe hot wallet address once assigned.
failureReasonstring or nullSet on failed.
webhookStatusstring or nullpending, received, failed, or null when the project has no endpoints.
feestring or nullActual network fee in the chain’s native token (SOL or ETH) once known, null before. Solana account rent is not included.

Errors

StatuscodeCause
400BAD_REQUESTField validation (including a numeric amount or a missing customerID); invalid Idempotency-Key; the chain is not on this install’s network; the amount is not representable in the currency’s decimals or outside the supported range; destination equals the hot wallet; USD value outside the supported range.
400PAYOUT_AMOUNT_BELOW_MINIMUMUSD value below the minimum. message names the minimum.
400PAYOUT_CURRENCY_DISABLEDThe currency is not enabled for the project, or its chain is inactive.
400PAYOUT_NO_HOT_WALLETNo active hot wallet of that chain family is assigned to the project.
401UNAUTHORIZEDInvalid API key.
409PAYOUT_DUPLICATE_REQUESTIdempotency key reused with different terms, or a matching payout inside the duplicate window.
409CONFLICTCustomer inactive; project inactive; hot wallet unavailable; hot wallet balance insufficient after pending reservations; the recipient’s hourly or daily limit exceeded.
429RATE_LIMITEDPer-key limit exhausted.
503EXCHANGE_RATE_UNAVAILABLENo fresh USD price for the currency. Stablecoins never fail this way.
503UNAVAILABLEA payout policy setting is invalid, or Redis is unreachable.

Creation, customer upsert, balance reservation and the webhook row are one transaction; on any failure nothing is stored.

GET /api/v1/withdrawal/{id}/merchant

curl https://<your-site>/api/v1/withdrawal/120/merchant \ -H "API-Key: 811b12035f0dfa8ffd62296df3c98b27"
ParameterInConstraints
idpathInteger, 1 to 2147483647.

200 OK with the same object as the create response, at its current state:

{ "id": 120, "createdAt": "2026-06-19T13:14:41.806Z", "updatedAt": "2026-06-19T13:18:02.114Z", "blockchainCode": "SOLANA", "currencyCode": "USDC", "currencyType": "token", "amount": "100", "priceInUSD": "1", "amountInUSD": "100.000000", "toAddress": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "tokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "recipientEmail": "[email protected]", "txHash": "5KjP2n8vQ4wR7tY1uI3oA6sD9fG2hJ4kL7zX0cV5bN8mQ1wE3rT6yU9iO2pA5sD8f", "status": "sent", "type": "payout_merchant", "attributes": "{}", "createdBy": "api", "fromAddress": "HotWa11etAddre55xxxxxxxxxxxxxxxxxxxxxxxxxxx", "failureReason": null, "webhookStatus": "received", "fee": "0.000005" }

Errors: 400 BAD_REQUEST when id is not an integer in range, 401 UNAUTHORIZED, 404 NOT_FOUND (Payout not found) when the id is unknown or belongs to another project, 429 RATE_LIMITED.

GET /api/v1/withdrawal/merchant

curl "https://<your-site>/api/v1/withdrawal/merchant?status[]=sent&status[]=processed&createdAfter=2026-09-01T00:00:00Z&limit=50&sortBy=id&order=ASC" \ -H "API-Key: 811b12035f0dfa8ffd62296df3c98b27"
ParameterTypeDefaultConstraints
limitinteger100, or payout.listPageCap if lower1 to 1000000 accepted; the effective page size is clamped to payout.listPageCap (default 100; 1 to 1000).
offsetinteger00 to 1000000.
orderstringDESCASC or DESC.
sortBystringcreated_atcreated_at, amount, id or status. Ties are broken by id in the same direction.
greaterThanIDintegerExclusive lower bound on id. Must be below lessThanID when both are given.
lessThanIDintegerExclusive upper bound on id.
createdAfter, createdBeforestringISO 8601 with timezone; exclusive. createdAfter must be before createdBefore.
updatedAfter, updatedBeforestringSame rules on updatedAt.
status[]string, repeatableAny of the nine statuses. At most 50 values.
blockchainCode[]string, repeatableSOLANA or ROBINHOOD. At most 50 values.
toAddress[], fromAddress[]string, repeatableExact addresses, at most 50 each. Case-insensitive on Robinhood Chain, case-sensitive on Solana.
recipientEmails[]string, repeatableEmail addresses, at most 50, case-insensitive.
searchstringLiteral substring (% and _ are not wildcards), case-insensitive, over destination, source address, recipient email and transaction hash. At most 128 characters.

Repeat array parameters with the bracket name: status[]=pending&status[]=sent. A single value without repetition also works.

200 OK, a bare JSON array of payout objects in the shape of the single read. Paginate with offset, or with greaterThanID and sortBy=id&order=ASC for a stable cursor, until a page returns fewer rows than the effective page size. Because a requested limit above the cap is silently clamped, compare against the number of rows you receive, not against the limit you sent.

Errors: 400 BAD_REQUEST for any parameter outside its constraints or an inverted range, 401 UNAUTHORIZED, 429 RATE_LIMITED, 503 UNAVAILABLE when payout.listPageCap is invalid.

Payout webhooks

Every persisted status transition posts one event to every active endpoint of the project, with the same headers, signature, retry schedule and delivery guarantees as the payment webhook. Idempotency on the sending side is per payout and status, so one status produces one delivery per endpoint. Resending an approval code or adding a note produces no event.

Database statusX-Recv-Event / event_typePayload status
pending-approval, pending-otp-verificationpayout.pending-approvalPENDING_APPROVAL, PENDING_OTP_VERIFICATION
pendingpayout.approvedPENDING
initiatedpayout.initiatedINITIATED
sentpayout.sentSENT
processedpayout.processedPROCESSED
failedpayout.failedFAILED
rejectedpayout.rejectedREJECTED
cancelledpayout.cancelledCANCELLED

The payload status is the database status in upper case with underscores, so it does not always match the event name (payout.approved carries PENDING).

{ "event_type": "payout.sent", "payout_id": 120, "network": "SOLANA", "token": "USDC", "amount": "100", "amount_usd": "100.000000", "email": "[email protected]", "address": "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin", "from_address": "HotWa11etAddre55xxxxxxxxxxxxxxxxxxxxxxxxxxx", "tx_hash": "5KjP2n8vQ4wR7tY1uI3oA6sD9fG2hJ4kL7zX0cV5bN8mQ1wE3rT6yU9iO2pA5sD8f", "status": "SENT", "withdrawal_type": "payout_merchant", "currency_type": "token", "created_at": 1750340081, "updated_at": 1750340282, "timestamp": 1750340282, "failure_reason": "", "network_fee_raw": "5000" }
FieldTypeMeaning
event_typestringThe event name, same as X-Recv-Event.
payout_idintegerThe payout id.
networkstringSOLANA or ROBINHOOD.
tokenstringCurrency code.
amountstringToken amount.
amount_usdstringUSD value at creation, six decimals.
emailstringRecipient email.
addressstringDestination.
from_addressstringHot wallet address, or "" before assignment.
tx_hashstringTransaction hash, or "" before broadcast.
statusstringUpper-case database status.
withdrawal_typestringAlways payout_merchant.
currency_typestringnative or token.
created_at, updated_at, timestampintegerUnix seconds: creation, last update, and when the event was recorded.
failure_reasonstringReason on failed, otherwise "".
network_fee_rawstring or nullActual network fee in base units of the native token once known. Not part of PayRam’s payload.

Handle payout events like payment events: verify the signature, deduplicate on X-Recv-Delivery, and expect duplicates and out-of-order arrivals. The initiated event is committed before the broadcast is attempted, so an initiated followed by nothing for a while means the worker is waiting for confirmations or for the next attempt, not that the payout was lost; read the payout to see its current status.

Last updated on