Mass-payout webhooks
Track mass-payout order events.
Mass-payout webhooks notify your endpoint when an order moves through its lifecycle — from creation to settlement or failure. They use a distinct signature scheme from payment IPNs.
Payment IPNs (HMAC-SHA-512, merchant-configured header) and mass-payout webhooks (HMAC-SHA-256, X-Payzum-Signature) use different signature algorithms and different headers. Do not reuse the same verification function for both.
Signature scheme
| Property | Value |
|----------|-------|
| Algorithm | HMAC-SHA-256 |
| Input | Raw request body bytes |
| Signature header | X-Payzum-Signature (lowercase hex) |
| Dedup header | X-Payzum-Event-Id — stable across retries |
The X-Payzum-Event-Id header contains an event ID with a pzwe_ prefix that is stable across delivery retries. Store and check this value to implement idempotent handling.
Verify in Node.js
import crypto from 'node:crypto'
export function verifyMassPayoutWebhook(rawBody, sigHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex')
const a = Buffer.from(expected, 'hex')
const b = Buffer.from(sigHeader, 'hex')
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
// In your handler:
app.post(
'/payzum/mass-payout/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.header('x-payzum-signature')
const eventId = req.header('x-payzum-event-id')
if (!sig || !verifyMassPayoutWebhook(req.body, sig, process.env.PAYZUM_MASSPAYOUT_SECRET)) {
return res.status(401).send('invalid signature')
}
// Deduplicate on eventId before processing
const payload = JSON.parse(req.body.toString('utf8'))
// payload.eventType, payload.eventId, payload.eventAt, payload.order
res.sendStatus(200)
},
)Common envelope
Every mass-payout event payload contains these top-level fields:
| Field | Type | Description |
|-------|------|-------------|
| eventType | string | One of the event types listed below |
| eventId | string | Stable pzwe_-prefixed ID for deduplication |
| eventAt | number | Unix timestamp (seconds) when the event was emitted |
| order | object | Order snapshot at the time the event fired |
Event types
| Event type | When it fires |
|------------|---------------|
| mass_payout.created | After POST /:id/confirm — order moves to pending_deposit |
| mass_payout.quote_refreshed | After a successful POST /:id/refresh-quote |
| mass_payout.deposit_detected | payzum receives a deposit callback |
| mass_payout.underfunded | Received amount is less than totalToSendRaw |
| mass_payout.overfunded | Received amount exceeds totalToSendRaw |
| mass_payout.batch_broadcasted | A batch transaction is broadcast on-chain |
| mass_payout.batch_confirmed | A batch reaches the required confirmation threshold |
| mass_payout.batch_failed | A batch fails permanently after exhausting retries |
| mass_payout.completed | All batches confirmed — order is terminal |
| mass_payout.partial_failed | Order ends with at least one failed batch |
| mass_payout.expired | Deposit was not received before expiresAt |
| mass_payout.cancelled | Order was cancelled by the merchant or an operator |
Deduplication
Use the eventId field (or the X-Payzum-Event-Id header) as a dedup key. The same event may be delivered more than once if your endpoint returned a non-2xx response during a previous attempt. Process each eventId at most once in your database or cache before acting on the payload.
Delivery contract
Each event has a 15-second timeout per attempt, retried on 5xx or network error with a 60-second delay, up to 5 attempts. A 4xx response permanently drops the delivery without retry. Events that exhaust all attempts are dead-lettered. See Retries & DLQ for re-enqueue instructions.