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.