§ 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
{
  "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

FieldTypeDescription
nuvouch.connection.acceptedeventA subject tuple was linked to a Nuvouch user.
nuvouch.connection.revokedeventA 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 env
import { 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 deliveryId and business externalRequestId.
  • 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.