§ Webhooks · Delivery

Delivery contract and callback headers

Each callback is an HTTPS POST of signed JSON to the callback URL configured on your integrator.

FieldTypeDescription
x-nuvouch-signatureheaderHMAC-SHA256 of the raw body, formatted as sha256=<hex>.
x-nuvouch-deliveryheaderStable delivery id reused across retries for the same logical callback.
§ Webhooks · Events

Approval callback events

FieldTypeDescription
nuvouch.approval_request.approvedeventApproval request was approved.
nuvouch.approval_request.deniedeventApproval request was denied.
nuvouch.approval_request.expiredeventNo decision before expiry.
nuvouch.approval_request.cancelledeventIntegrator cancelled pending request.
approved-event.json
{
  "event": "nuvouch.approval_request.approved",
  "payload": {
    "approvalRequestId": "req_2f5cd6dbb6784b9ab2f7",
    "externalRequestId": "payment_auth_001",
    "targetUserId": "usr_nuvouch_456",
    "status": "approved",
    "decisionAt": "2026-04-21T16:14:11.000Z",
    "decisionMethod": "biometric"
  },
  "timestamp": "2026-04-21T16:14:12.000Z"
}
§ Webhooks · Linking

Connection lifecycle events

FieldTypeDescription
nuvouch.connection.acceptedeventA subject tuple was linked to a Nuvouch user.
nuvouch.connection.revokedeventA previously linked subject tuple was revoked.
accepted-connection.json
{
  "eventType": "nuvouch.connection.accepted",
  "connectionId": "conn_123",
  "connectionSessionId": "conn_sess_123",
  "nuvouchUserId": "usr_nuvouch_456",
  "subjectId": "cus_123",
  "subjectLabel": "Ada Lovelace",
  "contextKey": "merchant:acct_live_001",
  "contextType": "merchant",
  "contextLabel": "Stripe Live Account",
  "connectedAt": "2026-04-21T16:05:00.000Z"
}
§ Webhooks · Parsing

Parse approval and connection payloads correctly

Parse callback body
sdk helper
import { parseNuvouchWebhookJson } from "@nuvouch/integrator-sdk";

const event = parseNuvouchWebhookJson(rawBody);

if (event.kind === "approval") {
  console.log(event.body.event);
  console.log(event.body.payload.externalRequestId);
}

if (event.kind === "connection") {
  console.log(event.body.eventType);
  console.log(event.body.connectionId);
}
§ 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 { verifyNuvouchWebhookSignatureFromEnv } from "@nuvouch/integrator-sdk";

const valid = await verifyNuvouchWebhookSignatureFromEnv({
  rawBody,
  signatureHeader: req.headers.get("x-nuvouch-signature"),
});
secret-manager.ts
sdk without env
import { verifyNuvouchWebhookSignature } from "@nuvouch/integrator-sdk";

const callbackSecret = await secrets.get("integrations/nuvouch/callback-secret");

const valid = await verifyNuvouchWebhookSignature({
  secret: callbackSecret,
  rawBody,
  signatureHeader: 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.

dedupe.ts
import { parseNuvouchWebhookJson } from "@nuvouch/integrator-sdk";

const deliveryId = req.headers.get("x-nuvouch-delivery");
if (deliveryId && await hasProcessed(deliveryId)) {
  return new Response("duplicate", { status: 200 });
}

const event = parseNuvouchWebhookJson(rawBody);
await processEvent(event);
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 business request id and delivery id.
  • Use signed callbacks, not status polling, to trigger the sensitive side effect.

For a complete payment authorization walkthrough, see integration guide.