Skip to content

Webhooks

UnizPay sends an HTTPS POST to your endpoint each time a payment changes status. Use webhooks to fulfil orders: they arrive even when the payer closes the payment page.

In the dashboard, open Developers → Webhooks and add your endpoint’s URL. Copy its signing secret: it is shown once. Endpoints belong to one mode, so add one in test mode and one in live mode. Every endpoint receives every event type.

Send test event on the endpoint’s page delivers an event of type test (with empty data) so you can check your handler.

EventWhen
payment.detectedA deposit was seen on-chain (not yet confirmed)
payment.confirmedDeposits reached the network’s confirmation depth
payment.settledThe payout was delivered to your payout address. Fulfil the order here.
payment.settled_rawThe conversion could not run (for example the price moved beyond your max conversion cost); the funds were delivered unconverted to your fallback address
payment.underpaidConfirmed deposits are below the amount due, minus your underpay tolerance
payment.expiredNothing was paid before the payment expired
payment.lateA deposit arrived after the payment expired or was canceled; it is still delivered
payment.canceledYou canceled the payment before any deposit
POST /webhooks/unizpay HTTP/1.1
Content-Type: application/json
User-Agent: UnizPay-Webhooks/1.0
UnizPay-Signature: t=1761912221,v1=5f2b9c0e…
{
"event_id": "evt_01K…",
"type": "payment.settled",
"created": "2026-10-31T12:03:41.512Z",
"livemode": true,
"data": { "object": "payment", "id": "pay_01K…", "payment_id": "order_1001", "status": "settled", … }
}

data is the full payment object at the time of the event, including its deposits and settlements.

The UnizPay-Signature header is t=<unix seconds>,v1=<signature>, where

signature = hex( HMAC-SHA256( signing_secret, t + "." + raw_request_body ) )

To verify a request:

  1. Read the raw request body, before any JSON parsing.
  2. Compute the signature with your signing secret and the header’s t.
  3. Accept the request if it equals any v1 value in the header. After you rotate the secret, events carry two v1 values (new and previous secret) until the next rotation.
  4. Reject it if t is more than 5 minutes away from your clock.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyUnizPay(rawBody, header, secret, toleranceSeconds = 300) {
let t = 0;
const signatures = [];
for (const part of (header ?? "").split(",")) {
const i = part.indexOf("=");
const key = part.slice(0, i).trim();
const value = part.slice(i + 1).trim();
if (key === "t") t = Number(value);
else if (key === "v1" && value) signatures.push(value);
}
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
return signatures.some((s) => {
const got = Buffer.from(s);
return got.length === expected.length && timingSafeEqual(got, expected);
});
}
// Express: keep the raw body for this route.
app.post("/webhooks/unizpay", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verifyUnizPay(raw, req.get("UnizPay-Signature"), process.env.UNIZPAY_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(raw);
// Deduplicate on event.event_id, then handle event.type …
res.sendStatus(200);
});
  • Success is any 2xx response within 10 seconds. Respond first, then do slow work in the background.
  • Retries: a failed delivery is retried with exponential backoff (30 s, 1 min, 2 min, …, at most 6 h apart), up to 15 attempts over about 32 hours. The dashboard shows every attempt with its response code.
  • At least once: the same event can arrive more than once. Store each event_id you handle and ignore repeats.
  • Order is not guaranteed. Use data.status (or fetch the payment) as the source of truth rather than the order events arrive in.