Skip to content

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 ​

RequirementDescription
A payable userOne you received user.onboarding_completed for, with their wallet_address
A webhook endpointRegistered and receiving events — see Receive and verify webhooks
A way to send USDCA wallet or custody provider that can send USDC on Base and give you the transaction hash

How it works ​

  1. Send the USDC
  2. Report the transfer
  3. Wait for the outcome
  4. Handle an expired payout

1. Send the USDC ​

Send USDC to the user's wallet_address, on the network for your environment:

EnvironmentNetworkChain idUSDC contract
StagingBase Sepolia845320x036CbD53842c5426634e7929541eC2318f3dCF7e
ProductionBase84530x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913

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_id is the user's id from Create user, not your external_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 its id.

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:

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:

reasonWhat happenedWhat 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 amountCheck 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 windowNo matching deposit arrived within 48 hoursCheck 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.