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
| Event | Subscribable | Delivered 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:
{
"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",
"amount": "100",
"grossAmount": "100",
"orderAmount": "100",
"fiatCurrency": null,
"fiatAmount": null,
"quoteRate": null,
"amountUnits": "100000000",
"txHash": "0xabc…",
"outputIndex": 0,
"depositAddress": "TXyz…",
"fromAddress": "TAbC…",
"blockNumber": 64512345,
"confirmations": 20,
"required": 19,
"confirmedAt": "2026-06-27T10:15:40.000Z"
}
}
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | Delivery id — your dedupe key. Also in the x-zaropay-delivery-id header. |
event | string | deposit.confirmed (or webhook.test for test probes) |
createdAt | string (ISO-8601) | When the payload was built |
data.depositId | string (uuid) | The deposit; matches reference_id in delivery logs |
data.amount | string | Merchant-facing amount: the order amount when one exists, else on-chain gross |
data.grossAmount | string | Actual on-chain amount received (always present) |
data.orderAmount | string | null | Expected order amount (USDT), or null for open addresses |
data.fiatCurrency | string | null | USD if the order was priced in fiat (see Pricing in USD); else null |
data.fiatAmount | string | null | The original fiat amount (e.g. "100"); null for USDT-priced orders |
data.quoteRate | string | null | The locked USDT-per-fiat rate at order creation; null otherwise |
data.txHash | string | null | On-chain tx hash |
data.depositAddress / data.fromAddress | string | null | To / from addresses |
data.confirmations / data.required | number | null | Confirmations at finalization |
data.confirmedAt | string | null | When it reached CONFIRMED |
Reconcile carefully
Compare data.grossAmount against data.orderAmount to detect under/overpayment. Match the payment
to your order via data.orderAmount plus the order_ref you set when creating the charge. For a
USD-priced order, amount / grossAmount / orderAmount are still USDT — the original
dollar figure is data.fiatAmount (use it for your own books, not for payment matching).
Request headers on every delivery
| Header | Value |
|---|---|
content-type | application/json |
user-agent | ZaroPay-Webhooks/1 |
x-zaropay-signature | t=<unixSeconds>,v1=<hex> — verify this |
x-zaropay-event | the event name |
x-zaropay-delivery-id | the delivery id (= payload id) |
Delivery semantics
- Respond
2xxto 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
2xxfirst, then do slow work asynchronously.
Best practices
- Verify the signature before trusting anything — see Signature verification.
- Dedupe on
id; make fulfilment idempotent. - Acknowledge quickly (
2xx), process in the background. - Use the delivery logs to debug from your side.