Skip to main content

Webhooks

Webhooks are how ZaroPay tells your backend a payment happened. They are the authoritative signal for fulfilment — always prefer them over the customer's browser redirect.

Events​

EventSubscribableDelivered today
deposit.confirmed✅ (default)✅ — the main payment event
deposit.late✅reserved (same payload shape, not yet emitted)
deposit.detected✅not emitted yet
withdrawal.confirmed✅not emitted yet
withdrawal.failed✅not emitted yet

Today, build against deposit.confirmed. If you subscribe with no eventTypes, you get deposit.confirmed by default.

Payload — deposit.confirmed​

This is the exact JSON body POSTed to your URL. The example is a client fee mode order of 100 USDT at a 1% fee (the customer pays the fee on top): the customer was asked for 101 and sent it, ZaroPay kept the 1 fee, and 100 reached the merchant's balance.

{
"id": "5f1c9d2e-7a4b-4c0a-9e21-3b8f0a1d2c34",
"event": "deposit.confirmed",
"createdAt": "2026-06-27T10:15:42.123Z",
"data": {
"depositId": "a1b2c3d4-…",
"merchantId": "m1m2m3m4-…",
"chain": "tron",
"currency": "USDT",
"source": "deposit",
"netAmount": "100",
"feeAmount": "1",
"grossAmount": "101",
"orderAmount": "100",
"paymentState": "exact",
"feeMode": "client",
"amount": "100",
"fiatCurrency": null,
"fiatAmount": null,
"quoteRate": null,
"fiatNetAmount": null,
"amountUnits": "101000000",
"txHash": "0xabc…",
"outputIndex": 0,
"depositAddress": "TXyz…",
"fromAddress": "TAbC…",
"blockNumber": "64512345",
"confirmations": 20,
"required": 19,
"confirmedAt": "2026-06-27T10:15:40.000Z"
}
}
FieldTypeNotes
idstring (uuid)Delivery id — your dedupe key. Also in the x-zaropay-delivery-id header.
eventstringdeposit.confirmed (or webhook.test for test probes)
createdAtstring (ISO-8601)When the payload was built
data.depositIdstring (uuid)The deposit; matches reference_id in delivery logs
data.sourcestringdeposit (paid to one of your deposit addresses) or external (a direct transfer to your settlement wallet — credited in full, feeAmount "0", feeMode / paymentState / orderAmount null)
data.netAmountstring | nullWhat you receive from this payment: grossAmount − feeAmount, i.e. the credit net of ZaroPay's platform fee. (If your account is on merchant gas mode, the sweep's network gas is debited from your balance separately, later — it is not part of this figure.) null only if ZaroPay had not finished crediting when the webhook was built (rare) — fetch the deposit via the API; never fulfil on null
data.feeAmountstring | nullThe ZaroPay platform fee charged on this payment ("0" when none)
data.grossAmountstringWhat arrived on-chain, before the fee (always present)
data.orderAmountstring | nullThe order amount you created the charge for (USDT), or null for an open address
data.paymentStatestring | nullexact, underpaid or overpaid — the on-chain amount vs what the customer was asked to send; null when no amount was quoted (open address)
data.feeModestring | nullWho bore the fee: client (customer paid it on top) or merchant (deducted from the order); null for a direct transfer to your settlement wallet
data.amountstringLegacy alias kept for older integrations: the order amount when one exists, else the on-chain gross. Prefer netAmount
data.fiatCurrencystring | nullUSD if the order was priced in fiat (see Pricing in USD); else null
data.fiatAmountstring | nullThe original fiat amount (e.g. "100"); null for USDT-priced orders
data.quoteRatestring | nullThe locked USDT-per-fiat rate at order creation; null otherwise
data.fiatNetAmountstring | nullFor fiat-priced orders: netAmount in the order's currency at the locked rate, rounded to cents (netAmount ÷ quoteRate). On an exact client-mode payment it equals fiatAmount (in merchant mode the fee is deducted: a $100 order at 1% → 99); on an under- or overpayment it is the fiat value of what you received. null for USDT-priced orders or until credited
data.txHashstring | nullOn-chain tx hash
data.depositAddress / data.fromAddressstring | nullTo / from addresses
data.blockNumberstring | nullBlock height (serialized as a decimal string)
data.confirmations / data.requirednumber | nullConfirmations at finalization
data.confirmedAtstring | nullWhen it reached CONFIRMED
Which amount do I use?

Two different questions, two different fields:

  • What did the customer pay for? Fulfil the order for orderAmount when paymentState is exact or overpaid (for an open address with no order, use grossAmount).
  • What do I receive? Book netAmount as your revenue — the credit net of ZaroPay's platform fee.
Fee modeCustomer asked to sendArrived (grossAmount)feeAmountFulfil for (orderAmount)You receive (netAmount)
client (fee on top)1011011100100
merchant (fee deducted)100100110099

In client mode the two are the same number because the customer paid the fee. Never credit the customer for grossAmount on a client-mode order — that books the customer's fee as their money. Match the payment to your order via data.orderAmount plus the order_ref you set when creating the charge. For a USD-priced order, netAmount / grossAmount / orderAmount are still USDT: fulfil for data.fiatAmount (the order in dollars) when paymentState is exact or overpaid, and book data.fiatNetAmount — what you received, already converted to dollars at the locked rate (a $100 order paid with 50 USDT at 1.002 USDT/USD → netAmount "49.5", fiatNetAmount "49.4").

Underpaid and overpaid payments​

paymentState compares grossAmount with the amount the customer was asked to send (client mode: the order plus the fee; merchant mode: the order). A customer who sends the quoted amount in full is charged exactly the quoted fee; otherwise ZaroPay charges the fee on the amount that arrived (a 1% order of 100 paid with 50 → fee 0.5, netAmount 49.5; paid with 200 → fee 2, netAmount 198). On an overpayment the fee is charged on the full amount received but capped at the surplus above the order (grossAmount − orderAmount), so you always receive at least orderAmount. ZaroPay credits whatever arrived and never refunds automatically — your integration decides. Rely on paymentState, not on comparing netAmount with orderAmount (in merchant mode an exact payment nets orderAmount − feeAmount, so netAmount < orderAmount does not mean the customer underpaid).

paymentStateWhat happenedSuggested handling
exactThe customer sent what was quotedFulfil for orderAmount; book netAmount
underpaidLess arrived than quotedDo not fulfil in full — book netAmount as a partial payment and ask the customer for the difference (a new charge), or refund it from your balance
overpaidMore arrived than quotedFulfil for orderAmount; book netAmount — the surplus (what arrived above the quote, net of the fee on it) is in your balance; refund it to the customer yourself if your policy requires
nullAn open address with no amount quotedFulfil for grossAmount; book netAmount

Request headers on every delivery​

HeaderValue
content-typeapplication/json
user-agentZaroPay-Webhooks/1
x-zaropay-signaturet=<unixSeconds>,v1=<hex> — verify this
x-zaropay-eventthe event name
x-zaropay-delivery-idthe delivery id (= payload id)

Delivery semantics​

  • Respond 2xx to acknowledge. Any other status, a timeout, or a redirect counts as failure.
  • Timeout: 10 seconds.
  • Retries: up to 5 attempts with backoff 60s → 300s → 1800s → 7200s; after the last failure the delivery is marked EXHAUSTED.
  • At-least-once: the same event can arrive more than once — dedupe on the payload id.
  • Redirects are not followed (anti-SSRF), so your endpoint must be the final URL.
  • Respond fast: acknowledge with 2xx first, then do slow work asynchronously.

Best practices​

  1. Verify the signature before trusting anything — see Signature verification.
  2. Dedupe on id; make fulfilment idempotent.
  3. Acknowledge quickly (2xx), process in the background.
  4. Use the delivery logs to debug from your side.