Skip to main content
Webhook event types and the signature scheme are still being finalized. Expect the signature to share the HMAC canonicalization scheme used for server-to-server requests. 🚧

Event types

EventWhen it fires
payment_intent.capturedProvider confirmed payout; merchant has been paid
payment_intent.failedFinal failure; intent will not be retried
payment_intent.refundedRefund completed end-to-end

Delivery

  • Webhooks are POSTed as JSON to your registered endpoint.
  • We retry with exponential backoff on non-2xx responses for up to 24 hours.
  • Order is not guaranteed. Treat your handler as idempotent — keyed on event_id.

Signature verification

Each webhook carries an X-Zennopay-Signature header. Verify it by re-computing HMAC-SHA256 over the canonical request (see Authentication) using the webhook signing secret issued during onboarding (distinct from your API signing secret).
import crypto from "node:crypto";

function verifyWebhook(headers, rawBody, secret) {
  const timestamp = headers["x-zennopay-timestamp"];
  const nonce = headers["x-zennopay-nonce"];
  const signature = headers["x-zennopay-signature"];
  const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
  const canonical = ["POST", "/webhook", timestamp, nonce, bodyHash].join("\n");
  const expected = crypto.createHmac("sha256", secret).update(canonical).digest("base64");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Always use a constant-time comparison.