§ Webhooks · Delivery
Delivery contract and callback headers
Each callback is an HTTPS POST of signed JSON to the callback URL configured on your integrator.
| Field | Type | Description |
|---|---|---|
| x-nuvouch-signature | header | HMAC-SHA256 of the raw body, formatted as sha256=<hex>. |
| x-nuvouch-delivery | header | Stable delivery id reused across retries for the same logical callback. |
§ Webhooks · Events
Approval callback events
| Field | Type | Description |
|---|---|---|
| nuvouch.approval_request.approved | event | Approval request was approved. |
| nuvouch.approval_request.denied | event | Approval request was denied. |
| nuvouch.approval_request.expired | event | No decision before expiry. |
| nuvouch.approval_request.cancelled | event | Integrator cancelled pending request. |
approved-event.json
{
"type": "nuvouch.approval_request.approved",
"deliveryId": "dlv_2f5cd6dbb6784b9ab2f7",
"createdAt": "2026-04-21T16:14:12.000Z",
"data": {
"approvalRequest": {
"id": "req_2f5cd6dbb6784b9ab2f7",
"externalRequestId": "payment_auth_001",
"status": "approved",
"decidedAt": "2026-04-21T16:14:11.000Z",
"decision": {
"value": "approve",
"method": "biometric"
}
}
}
}§ Webhooks · Linking
Connection lifecycle events
| Field | Type | Description |
|---|---|---|
| nuvouch.connection.accepted | event | A subject tuple was linked to a Nuvouch user. |
| nuvouch.connection.revoked | event | A previously linked subject tuple was revoked. |
accepted-connection.json
{
"type": "nuvouch.connection.accepted",
"deliveryId": "dlv_3e8cd9dbb6784b9ab2f8",
"createdAt": "2026-04-21T16:05:01.000Z",
"data": {
"connection": {
"id": "conn_123",
"status": "active",
"subject": {
"id": "cus_123",
"label": "Ada Lovelace"
},
"source": {
"key": "merchant:acct_live_001",
"name": "Stripe Live Account"
},
"linkedAt": "2026-04-21T16:05:00.000Z"
}
}
}§ Webhooks · Parsing
Parse approval and connection payloads correctly
Parse callback body
sdk helper
import { Nuvouch } from "@nuvouch/node";
const nuvouch = new Nuvouch({ apiKey: process.env.NUVOUCH_API_KEY });
const event = nuvouch.webhooks.verify({
rawBody,
signature: req.headers.get("x-nuvouch-signature"),
secret: process.env.NUVOUCH_CALLBACK_SECRET!,
});
if (nuvouch.webhooks.isApprovalEvent(event)) {
console.log(event.type);
console.log(event.data.approvalRequest.externalRequestId);
}
if (nuvouch.webhooks.isConnectionEvent(event)) {
console.log(event.type);
console.log(event.data.connection.id);
}§ Webhooks · Security
Verify signature before processing
Each delivery includes x-nuvouch-signature and x-nuvouch-delivery. Verify signature over raw request body before parsing business fields.
Verify webhook signature
recommended
import { Nuvouch } from "@nuvouch/node";
const nuvouch = new Nuvouch({ apiKey: process.env.NUVOUCH_API_KEY });
const event = nuvouch.webhooks.verify({
rawBody,
signature: req.headers.get("x-nuvouch-signature"),
secret: process.env.NUVOUCH_CALLBACK_SECRET!,
});secret-manager.ts
sdk without envimport { Nuvouch } from "@nuvouch/node";
const nuvouch = new Nuvouch({ apiKey: process.env.NUVOUCH_API_KEY });
const callbackSecret = await secrets.get("integrations/nuvouch/callback-secret");
const event = nuvouch.webhooks.verify({
secret: callbackSecret,
rawBody,
signature: req.headers.get("x-nuvouch-signature"),
});§ Webhooks · Idempotency
Deduplicate delivery attempts
The x-nuvouch-delivery header is stable across retries for the same logical callback. Persist processed delivery IDs and short-circuit duplicates. For approval events, also persist and gate the business side effect by data.approvalRequest.externalRequestId.
dedupe.ts
const deliveryId = req.headers.get("x-nuvouch-delivery");
if (deliveryId && await hasProcessed(deliveryId)) {
return new Response("duplicate", { status: 200 });
}
const event = nuvouch.webhooks.verify({
rawBody,
signature: req.headers.get("x-nuvouch-signature"),
secret: process.env.NUVOUCH_CALLBACK_SECRET!,
});
const createdAt = Date.parse(event.createdAt);
if (Number.isNaN(createdAt) || Math.abs(Date.now() - createdAt) > 5 * 60 * 1000) {
return new Response("stale delivery", { status: 400 });
}
await runOnce(event.deliveryId, async () => {
await processEvent(event);
});
if (event.type === "nuvouch.approval_request.approved") {
await runOnce(event.data.approvalRequest.externalRequestId, async () => {
await executeApprovedAction(event.data.approvalRequest);
});
}
if (deliveryId) await markProcessed(deliveryId);§ Webhooks · Retries
Retry model and handler expectations
Delivery behavior
- Any non-2xx response is treated as a failed attempt.
- Timeouts and transport errors are also treated as failures and retried automatically.
- Retry delays are approximately 30s, 120s, 480s, 1800s, and 7200s.
- A 2xx response marks the delivery as succeeded and stops further retries.
- After the final failed attempt, the delivery is marked dead.
Handler guidance
- Verify signature, dedupe, then enqueue async business processing.
- Return 2xx quickly once event is safely persisted.
- Design side effects to be idempotent by both
deliveryIdand businessexternalRequestId. - Enforce a replay window using signed envelope
createdAt; five minutes is a practical default for direct handlers. - Use signed callbacks, not status polling, to trigger the sensitive side effect.
For a complete payment authorization walkthrough, see integration guide.