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
| Event | Fired when |
|---|---|
payment.succeeded | A card payment completed. |
payment.failed | A card payment failed (e.g. declined). |
payment.refunded | A refund was issued (full or partial). |
test.event | A 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");<?php
function verify_invoicly_signature(string $secret, ?string $header, string $rawBody, int $toleranceSec = 300): bool {
if (!preg_match('/^t=(\d+),v1=([a-f0-9]+)$/', (string) $header, $m)) {
return false;
}
$timestamp = (int) $m[1];
// Replay protection: reject stale signatures.
if (abs(time() - $timestamp) > $toleranceSec) {
return false;
}
$expected = "t={$timestamp},v1=" . hash_hmac("sha256", "{$timestamp}.{$rawBody}", $secret);
return hash_equals($expected, (string) $header); // constant-time compare
}
// $rawBody = file_get_contents("php://input");
// if (!verify_invoicly_signature(getenv("INVOICLY_WEBHOOK_SECRET"),
// $_SERVER["HTTP_INVOICLY_SIGNATURE"] ?? null, $rawBody)) {
// http_response_code(401); exit("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?