Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.request.network/llms.txt

Use this file to discover all available pages before exploring further.

What youโ€™ll build

A webhook handler that receives signed payment events from Request Network, verifies the signature, and triggers your downstream systems โ€” order fulfillment, invoice closeout, accounting entries, customer email. Polling-free, idempotent, retry-safe. Audience: any backend integrating Request Network where payment events drive state changes downstream.

The 12 events

CategoryEventWhen it fires
Payment (core)payment.confirmedPayment fully settled on-chain
payment.partialPartial payment received, more expected
payment.failedPayment execution failed (recurring, cross-chain)
payment.refundedPayment refunded to payer
Payment (Client ID)payment.confirmed.client_idSame as payment.confirmed, request was created via Client ID โ€” payload includes clientId and origin
payment.partial.client_idClient ID-scoped partial
Payment (Checkout)payment.confirmed.checkoutSame as payment.confirmed, request originated from a Secure Payment link
payment.partial.checkoutSecure Payment-scoped partial
Processingpayment.processingCrypto-to-fiat offramp in progress (with detailed subStatus)
Requestrequest.recurringA new recurring billing cycle fired
Compliancecompliance.updatedKYC or agreement status changed
Bank detailspayment_detail.updatedBank account verification status changed
For the full payload schemas, see the Webhooks reference.

Setup

1

Get a Client ID

Complete steps 1โ€“3 of the Quickstart. Note your clientId.
2

Register your webhook URL

POST https://auth.request.network/v1/webhook with header x-client-id: <yours> and body { "url": "https://yourapp.com/webhooks/request-network" }.Save the returned secret immediately โ€” itโ€™s only shown once.
3

Test delivery

Fire a test event from the auth API docs with body { "eventType": "payment.confirmed" }. The request will arrive with header x-request-network-test: true.

Handler โ€” reference implementation

A signature-verifying Express handler with idempotency. Hardened for production: verifies against the raw body, uses constant-time comparison, deduplicates on x-request-network-delivery.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
import { Redis } from "ioredis";

const app = express();
const redis = new Redis(process.env.REDIS_URL!);
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;

app.post(
  "/webhooks/request-network",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.headers["x-request-network-signature"] as string;
    const deliveryId = req.headers["x-request-network-delivery"] as string;
    const isTest = req.headers["x-request-network-test"] === "true";

    if (!signature || !deliveryId) {
      return res.status(400).send("missing headers");
    }

    // 1. Verify signature against the RAW body โ€” never re-stringify
    const expected = createHmac("sha256", WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

    const sigBuf = Buffer.from(signature, "hex");
    const expBuf = Buffer.from(expected, "hex");

    if (
      sigBuf.length !== expBuf.length ||
      !timingSafeEqual(sigBuf, expBuf)
    ) {
      return res.status(401).send("invalid signature");
    }

    // 2. Idempotency: dedupe on delivery ID
    const claim = await redis.set(
      `webhook:${deliveryId}`,
      "1",
      "EX",
      86400, // 24h
      "NX",
    );
    if (!claim && !isTest) {
      return res.status(200).send("duplicate, ignored");
    }

    // 3. Parse and route
    const event = JSON.parse(req.body.toString("utf8"));

    try {
      await handleEvent(event);
    } catch (err) {
      // Return non-2xx to trigger retry (1s, 5s, 15s โ€” up to 3 retries)
      console.error("handler failed", err);
      return res.status(500).send("handler error");
    }

    res.status(200).send("ok");
  },
);

async function handleEvent(event: any) {
  switch (event.event) {
    case "payment.confirmed":
    case "payment.confirmed.client_id":
    case "payment.confirmed.checkout":
      await markOrderPaid(event.requestId, event.txHash);
      break;

    case "payment.partial":
    case "payment.partial.client_id":
    case "payment.partial.checkout":
      await recordPartialPayment(
        event.requestId,
        event.amount,
        event.totalAmountPaid,
      );
      break;

    case "payment.failed":
      await flagFailedPayment(event.requestId);
      break;

    case "request.recurring":
      await onRecurringInvoice(event.originalRequestId, event.requestId);
      break;

    case "compliance.updated":
      await syncKycStatus(event.clientUserId, event.kycStatus);
      break;

    // ... others
  }
}

Headers reference

HeaderDescription
x-request-network-signatureHMAC-SHA256 of the raw JSON body, hex-encoded
x-request-network-deliveryULID โ€” use as idempotency key
x-request-network-retry-count0โ€“3, current retry attempt
x-request-network-testtrue only for /v1/webhook/test deliveries

Retry policy

AttemptDelayCumulative time
0 (initial)โ€”t=0
11st+1s
25st+6s
315st+21s
After 4 total attempts (initial + 3 retries) the delivery is dropped. Triggers: any non-2xx response, timeout, connection error. Default request timeout is 5s.

Common patterns

Idempotency

The same payment.confirmed event might arrive twice (network blip, retry overlap). Dedupe on x-request-network-delivery.

Routing by Client ID

If your platform has many merchants, give each their own Client ID. The webhook payload includes clientId so you can route events to the right tenant.
async function markOrderPaid(requestId: string, txHash: string) {
  const order = await db.orders.findOne({ where: { requestId } });
  if (!order) return; // not ours
  await db.orders.update({ where: { id: order.id }, data: { paidAt: new Date(), txHash } });
}

Slack alerts on failure

case "payment.failed":
  await fetch(SLACK_WEBHOOK, {
    method: "POST",
    body: JSON.stringify({
      text: `:warning: Payment failed for request ${event.requestId}`,
    }),
  });
  break;

Crypto-to-fiat status tracking

The payment.processing event includes a subStatus field that progresses through initiated โ†’ pending_internal_assessment โ†’ ongoing_checks โ†’ sending_fiat โ†’ fiat_sent. Surface this in your UI so the payee sees real-time offramp progress.

Local development

Use ngrok to expose localhost during development:
ngrok http 3000
# Pass the https://xxxxx.ngrok-free.app URL to POST /v1/webhook (above)
Local URLs (localhost, 127.0.0.1) are accepted by the auth API for testing. HTTPS is required in production.

Webhooks reference

Full payload schemas for every event type.

Webhooks & Events

High-level concepts and event categories.