What the crypto mass payout API actually does
A single POST /v1/payout call reserves the funds for the whole batch, validates every recipient, and creates a payout batch you can track. That is the honest shape of a crypto mass payout API on a gateway with an internal Payora balance: the API automates submission, validation, status and webhooks, while release follows the approval, signing or release flow configured for the account. A created batch is not the same thing as confirmed on-chain payout, and recipients should not be marked paid until status and transaction evidence prove it. If that trade-off sounds acceptable, the API turns a manual payroll or affiliate run into two or three lines of code.
This is a developer guide. It copies the exact contract from the API docs so you can ship against it today: the endpoints, the HMAC signing, the Idempotency-Key, the strict 422 validation, the payout.sent webhook, and how cancel releases the held balance. If you want the product overview first, read the mass payouts page.
Why submission and release are separate
When you submit a batch — from the cabinet or through the API — Payora reserves the total plus the per-item move fee on your internal balance, so the batch cannot be overspent. The batch then follows the approval, signing or release flow configured for the account. Treat this as a reviewed payout workflow, not as proof that money has already landed with every recipient.
The security model you want is simple: do not make "API call, coins instantly gone" your mental model unless that is explicitly the product you have chosen. Fully automatic hot-wallet-style payout products can be faster, but they also raise the blast radius of an account or server compromise. Payora's current operational steps should be checked in the product docs and account settings before you treat a batch as released.
Authentication and request signing
The crypto mass payout API uses the same auth as the rest of Payora. Every request carries three headers: X-Payora-Key, X-Payora-Timestamp (Unix seconds, ±120s skew), and X-Payora-Signature. The signature is an HMAC-SHA256 over the timestamp, a literal dot, and the exact raw request body:
X-Payora-Signature = hex(HMAC-SHA256(api_secret, timestamp + "." + raw_body))Sign the bytes you actually send, before any reserialization. If your HTTP client re-encodes the JSON, capture the raw body first and hash that. This is the identical scheme used to verify inbound webhooks, covered in HMAC-SHA256 webhook signature verification.
POST /v1/payout: submit a batch
Send a currency, up to 500 items, and an optional note. Each item is an address, an amount as a string, and an optional tag — the XRP destination tag or XLM memo when the asset needs one.
curl -X POST https://api.payora.money/v1/payout \
-H "X-Payora-Key: pk_live_9f3c8a12" \
-H "X-Payora-Timestamp: 1753280400" \
-H "X-Payora-Signature: 7b1e0c4d9a55f2..." \
-H "Idempotency-Key: payroll-2026-07" \
-H "Content-Type: application/json" \
-d '{
"currency": "USDT",
"items": [
{"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "amount": "250.00"},
{"address": "TXk9mP2v7wQ3nR5tYb1cD8eF4gH6jK0lMn", "amount": "1800.00", "tag": ""}
],
"note": "July contractors"
}'On success you get 201 with a batch you can track. The total is the sum of amounts, fee is the move fee of your plan (1.5% on Free) plus the coin’s network fee, and both are already held against your balance:
{
"batch_id": "po_7Qd2K8xM4b",
"status": "pending",
"currency": "USDT",
"item_count": 2,
"total": "2050.00",
"fee": "20.50",
"items": [
{"index": 0, "address": "TR7NHqjeKQ...", "amount": "250.00", "status": "pending"},
{"index": 1, "address": "TXk9mP2v7w...", "amount": "1800.00", "status": "pending"}
]
}Accepting payments is still 0%; the move fee only applies when money leaves. Network fees are separate and paid on-chain at broadcast time.
Strict validation: no silently dropped recipient
Validation is all-or-nothing on purpose. If any address is malformed for the chosen network, or any amount carries more decimal places than the coin supports, the entire batch is rejected with 422 and the offending indexes. Nothing is queued, nothing is held.
HTTP/1.1 422 Unprocessable Entity
{
"error": "validation_failed",
"invalid_items": [3, 17],
"message": "invalid address at index 3; amount exceeds coin precision at index 17"
}This is the behaviour you want for payroll. A gateway that quietly truncated 1800.123456789 or skipped one bad row would underpay or drop a person, and you would not notice until someone complained. Fix the flagged indexes and resubmit the whole batch.
Idempotency: retries never double-pay
Networks time out. A payout request that fails midway is dangerous to retry blindly, so send an Idempotency-Key header — any stable, unique string for that logical batch (a payroll period, an invoice group). If a request with the same key arrives again, Payora returns the original batch instead of creating a second one. Reuse the key for every retry of the same batch; use a fresh key for a genuinely new batch.
- Same key, same batch — safe to retry after a timeout; you get the first result back.
- New key — a new batch, a new hold on your balance.
- Store the returned
batch_idas soon as you have it, so you can reconcile even if the response was lost.
Reconcile with GET /v1/payout/{id}
After the configured release flow advances, poll GET /v1/payout/{id} for the current status and a per-item tx_hash when transaction evidence is available. GET /v1/payout lists recent batches for dashboards and audits. Store the batch_id and each item index against your own records so every recipient maps to evidence you can show them.
Need to pull one back before release? POST /v1/payout/{id}/cancel cancels a still-pending batch and releases the held balance in full. Once items are released on-chain they are final, like any crypto transaction — cancel only works while the batch is pending.
The payout.sent webhook
Rather than polling forever, subscribe to payout.sent. After the configured release flow completes and Payora has transaction evidence for the batch items, Payora fires a signed webhook using the same HMAC signing as invoice.paid. Verify it the same way — recompute the signature over timestamp, dot, raw body and compare in constant time:
$expected = hash_hmac('sha256', $timestamp . '.' . $raw_body, $webhook_secret);
if (!hash_equals($expected, $signature_header)) { http_response_code(400); exit; }
// payload.event == "payout.sent", payload.batch_id, payload.items[].tx_hashOnly after that check passes should you mark the payroll run complete, email receipts, or update ledgers. An unverified webhook is just an untrusted HTTP request.
A minimal integration checklist
- Create an account and generate API keys at register; fund the internal balance in the currency you will pay out.
- Build the raw JSON body, sign it (timestamp + "." + body) with HMAC-SHA256, attach the three headers plus an
Idempotency-Key. POST /v1/payout; on201persist thebatch_id, on422fix the flagged indexes and resend.- Complete the approval, signing or release steps configured for the account.
- Verify the
payout.sentwebhook, or pollGET /v1/payout/{id}, and record eachtx_hash.
That is the whole loop. The API removes the tedious, error-prone part — building, validating and tracking a 500-line batch — while your operational controls decide when a reviewed batch is actually released.
Start building
Read the Mass Payout API section of the docs, grab keys at register, and see the feature end to end on the mass payouts page. You can automate batch creation today and keep payout release tied to the controls configured for your account.



Comments