Skip to content

Webhooks

Receive real-time event notifications — event types and payloads, headers, signature verification in Node and PHP, replay protection, retries, and testing.

Webhooks

Webhooks tell your server when money moves, so you don't have to poll. Register an endpoint in Settings → Webhooks (owner role), subscribe to events, and Biz Invoice Pay POSTs signed JSON to your URL.

Events

EventFired when
payment.succeededA card payment completed.
payment.failedA card payment failed (e.g. declined).
payment.refundedA refund was issued (full or partial).
test.eventA test event you send from the dashboard.

Payload shape

Every delivery wraps the event in an envelope:

{
  "id": "evt_9f3a2b1c4d5e6f70",
  "type": "payment.succeeded",
  "created": "2026-10-14T21:05:00.000Z",
  "data": {
    "payment": {
      "id": "pay_7d2e9a41",
      "amount_cents": 5000,
      "application_fee_cents": 275,
      "net_to_seller_cents": 4725,
      "status": "succeeded",
      "session_id": "chk_9f3a2b1c",
      "customer_email": "sam@example.com"
    }
  }
}

A payment.refunded payload adds a refund object with the refund's id, amount_cents, and status. A test.event payload has "test": true and a short message instead of a payment.

Headers

Each delivery sends:

Content-Type: application/json
Biz Invoice Pay-Event: payment.succeeded
Biz Invoice Pay-Delivery: wdl_a1b2c3d4e5f6
Biz Invoice Pay-Signature: t=1728912300,v1=9f3a…

Signature verification

The signature is HMAC-SHA256 of "<timestamp>.<raw body>", keyed with your endpoint's secret, formatted as t=<timestamp>,v1=<hex>. Verify it on every delivery, compare timestamps to prevent replay, and use a constant-time comparison.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyBiz Invoice PaySignature(secret, signatureHeader, rawBody, toleranceSec = 300) {
  const match = /^t=(\d+),v1=([a-f0-9]+)$/.exec(signatureHeader || "");
  if (!match) return false;
  const timestamp = Number(match[1]);
  // Replay protection: reject stale signatures.
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false;
  const expected = `t=${timestamp},v1=${createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex")}`;
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  return a.length === b.length && timingSafeEqual(a, b);
}

// In your route handler (use the RAW body, before JSON parsing):
// const ok = verifyBiz Invoice PaySignature(process.env.INVOICLY_WEBHOOK_SECRET,
//   req.headers["invoicly-signature"], rawBody);
// if (!ok) return res.status(401).send("bad signature");

Verify the signature against the raw request body, not a re-serialized JSON object — re-serialization changes whitespace and breaks the signature.

Retry schedule

Your endpoint must return a 2xx status within 10 seconds. On failure we retry with backoff: 1 min, 5 min, 30 min, 2 hours, 12 hours, then mark the delivery exhausted. Each delivery carries a stable Biz Invoice Pay-Delivery id.

Idempotent handling

Retries mean you can receive the same event twice. Make your handler idempotent:

  • Key your processing on the event id (evt_…) — store processed ids and skip duplicates.
  • Don't fulfill an order twice: check "already fulfilled for payment pay_…" before acting.

Send a test event

In Settings → Webhooks, click Send test event on an endpoint. It queues a real signed payment.succeeded delivery with "test": true in the payload and a dummy payment (pay_test, 1000¢), so you can verify your endpoint end-to-end without touching money. Delivery attempts and status codes show in the endpoint's delivery log.

Last updated: October 14, 2026

Was this page helpful?