Appearance
Report a payout
Send a user USDC, tell Zay about it, and know whether it arrived.
Already reported one in Get started? This guide covers what the tutorial skipped: the real transfer, safe retries, and what to do when a payout expires.
Requirements
| Requirement | Description |
|---|---|
| A payable user | One you received user.onboarding_completed for, with their wallet_address |
| A webhook endpoint | Registered and receiving events — see Receive and verify webhooks |
| A way to send USDC | A wallet or custody provider that can send USDC on Base and give you the transaction hash |
How it works
1. Send the USDC
Send USDC to the user's wallet_address, on the network for your environment:
| Environment | Network | Chain id | USDC contract |
|---|---|---|---|
| Staging | Base Sepolia | 84532 | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| Production | Base | 8453 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
Keep the transaction hash. USDC has 6 decimals, and Zay matches the amount exactly: report the same amount you sent, with no more than 6 decimal places.
Zay does not confirm deposits yet — see How payouts work.
2. Report the transfer
Once the transfer is broadcast, report it with Report a payout:
bash
curl -X POST "https://api-connect.zay.me/merchants/v1/payouts/report" \
-H "Content-Type: application/json" \
-H "zay-api-key: $ZAY_API_KEY" \
-H "zay-timestamp: $TS" \
-H "zay-signature: $SIG" \
-H 'Idempotency-Key: "60eb2155-c3b4-43d7-9451-0fe1cc009d85"' \
-d '{
"user_id": "2c9596e6-7c48-4fe9-b957-738bd2e04adc",
"payout": {
"amount": "25.5",
"currency_code": "USDC",
"transaction_hash": "0x9f2c…",
"custom_metadata": {"invoice_ref": "INV-42"}
}
}'user_idis the user'sidfrom Create user, not yourexternal_id.- Put your own reference in
custom_metadata. It comes back on the payout and in every payout event, so you can match them to your records without storing Zay's ids. - The payout is created in
pending. Save itsid.
Retrying safely
Generate the Idempotency-Key once per payout and store it before you send the request. If the request times out or you get no response, retry with the same key (re-sign it with a fresh timestamp — the signature is not part of the idempotency check).
Zay keeps the original response for 30 seconds. A retry within that window returns the original payout instead of creating a second one; a retry with the same key after it creates a new payout. If more than 30 seconds have passed, first check whether the payout exists — List payouts for the user and look for your transaction_hash or custom_metadata — and only report again if it doesn't.
A 429 with retry-after: 2 means the original request is still being processed; wait two seconds and retry with the same key.
3. Wait for the outcome
Zay looks for the matching deposit every minute and sends one of two events:
payout.received— the deposit matched. The payout is done.payout.expired— it didn't. See the next step.
Both carry the full Payout object, including your custom_metadata. Until confirmation is live, every payout ends in payout.expired after 48 hours.
If an event doesn't arrive — your endpoint was down, say — read the payout with Retrieve payout, or reconcile in bulk with List payouts filtered by status=pending.
4. Handle an expired payout
reason tells you why:
reason | What happened | What to do |
|---|---|---|
deposit did not match the reported payout (wrong amount, token, or failed transaction) | Zay found the transaction, but it failed, sent another token, or sent a different amount | Check the transaction. If the USDC never left, send it again and report the new hash. If the amount was off, report a new payout with the amount actually sent |
deposit not confirmed within the verification window | No matching deposit arrived within 48 hours | Check that the transaction was mined on the right network and to the user's wallet_address |
An expired payout stays expired. Each fix is a new payout, reported with a new Idempotency-Key.
Related
- How payouts work — the lifecycle and what Zay checks
- Payouts — the Payout object and its endpoints
- Handle errors and retries — which errors to retry and how