> ## Documentation Index
> Fetch the complete documentation index at: https://requestnetwork-08-31-chore-document-current-webhook-delivery.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Register signed webhook endpoints, identify who receives each event, and process payloads safely.

## Overview

Webhooks notify your server when Request Network processes a payment, completes KYT screening, or finishes hosted onboarding. Every delivery is an HMAC-signed `POST` request.

## Choose who receives notifications

A platform owns its Client IDs and Secure Payments. When it links a Client ID to an orchestrator, the orchestrator can create Secure Payments on the platform's behalf. Some events are then delivered to both the platform's Client ID endpoint and the linked orchestrator's endpoint.

You do not subscribe an endpoint to individual event types. An active endpoint receives the events available to the Client ID or orchestrator that registered it.

| Recipient            | Authentication                                    | Receives                                                                                           |
| -------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Platform's Client ID | `x-client-id` or the platform's Dashboard session | Events for Secure Payments created with that Client ID.                                            |
| Orchestrator         | `x-orchestrator-key`                              | Hosted-onboarding events and events for Secure Payments it created on behalf of a linked platform. |

Register each role separately, even if both roles use the same callback URL. Each registration has its own signing secret.

<Note>
  New platform endpoints are scoped to a Client ID. Existing platform-wide endpoints continue to receive platform events, but you cannot create new platform-wide endpoints through the current registration flow.
</Note>

## Register a platform Client ID webhook

Use the platform's Client ID to register an endpoint:

```bash theme={null}
curl -X POST "https://auth.request.network/v1/webhook" \
  -H "Content-Type: application/json" \
  -H "x-client-id: cli_YOUR_CLIENT_ID" \
  -d '{ "url": "https://platform.example.com/webhooks/request-network" }'
```

**Response (201 Created):**

```json theme={null}
{
  "id": "01KJC2WX8EH4MP3DHZB2YQ7N9G",
  "secret": "f3c189a4b5e6d7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2"
}
```

<Warning>
  Save `secret` when you create the endpoint. Request Network returns it only once. Use HTTPS in production; `localhost` is accepted for local development.
</Warning>

### Manage platform endpoints

All endpoints accept `x-client-id` and operate on the webhooks owned by that Client ID.

| Method   | Path                     | Purpose                                                            |
| -------- | ------------------------ | ------------------------------------------------------------------ |
| `GET`    | `/v1/webhook`            | List webhooks for this Client ID                                   |
| `PUT`    | `/v1/webhook/:webhookId` | Toggle active / inactive                                           |
| `DELETE` | `/v1/webhook/:webhookId` | Permanently delete                                                 |
| `POST`   | `/v1/webhook/test`       | Body `{ "eventType": "payment.confirmed" }` — fire a test delivery |

Open the [Auth API Scalar docs](https://auth.request.network/open-api/#tag/webhook) to call these interactively with your wallet session. Signing in to the [Dashboard](https://dashboard.request.network) sets the session cookie shared across Request Network services.

### Local Development

Use [ngrok](https://ngrok.com/docs/traffic-policy/getting-started/agent-endpoints/cli) to receive webhooks locally, then pass the public URL to `POST /v1/webhook`:

```bash theme={null}
ngrok http 3000
# Use the HTTPS URL (e.g., https://abc123.ngrok.io/webhook) as the webhook URL
```

## Orchestrator webhooks

Orchestrator webhooks are owned by your orchestrator. Register and manage them with `x-orchestrator-key`, not a platform's `x-client-id`.

Register an endpoint before you send a hosted onboarding URL to a platform. Its active endpoints receive:

* [`client_id.linked`](#client-id-linked) after the platform completes hosted onboarding.
* [`payment.confirmed`](#payment-confirmed), [`kyt.screening.completed`](#kyt-screening-completed), and [`secure_payment.user_event`](#secure-payment-user-event) for Secure Payments the orchestrator created on a linked platform's behalf.

### Register an endpoint

```bash theme={null}
curl -X POST "https://api.request.network/v2/orchestrators/webhooks" \
  -H "x-orchestrator-key: orc_YOUR_ORCHESTRATOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://partner.example.com/webhooks/request-network" }'
```

The response includes the endpoint and a signing secret:

```json theme={null}
{
  "webhook": {
    "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "url": "https://partner.example.com/webhooks/request-network",
    "isActive": true,
    "createdAt": "2026-08-14T10:00:00.000Z"
  },
  "secret": "4f2c5a8d1b3e6f709c2d4a7b0e1f3c5d8a2b4e6f9c1d3a5b7e0f2c4d6a8b1e3f"
}
```

<Warning>
  Save the signing secret when you register the endpoint. Request Network returns it only once and never includes it in list, deactivate, or reactivate responses.
</Warning>

Verify the `x-request-network-signature` HMAC-SHA256 header against the raw request body before you process an event. See [Signature Verification](#signature-verification). Webhook deliveries may be retried, so your endpoint must safely handle the same notification more than once.

### Test your endpoint

Send a signed mock event to every active endpoint:

```bash theme={null}
curl -X POST "https://api.request.network/v2/orchestrators/webhooks/test" \
  -H "x-orchestrator-key: orc_YOUR_ORCHESTRATOR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "eventType": "client_id.linked" }'
```

```json theme={null}
{
  "sent": 1,
  "failed": 0
}
```

Test deliveries include `x-request-network-test: true`. They use the same signing process and payload shape as a real event, with placeholder values.

### Manage endpoints

List every endpoint registered to your orchestrator, including inactive ones:

```bash theme={null}
curl "https://api.request.network/v2/orchestrators/webhooks" \
  -H "x-orchestrator-key: orc_YOUR_ORCHESTRATOR_KEY"
```

To stop delivery without removing the endpoint, deactivate it:

```bash theme={null}
curl -X DELETE "https://api.request.network/v2/orchestrators/webhooks/01ARZ3NDEKTSV4RRFFQ69G5FAV" \
  -H "x-orchestrator-key: orc_YOUR_ORCHESTRATOR_KEY"
```

To resume delivery, reactivate the same endpoint:

```bash theme={null}
curl -X PATCH "https://api.request.network/v2/orchestrators/webhooks/01ARZ3NDEKTSV4RRFFQ69G5FAV" \
  -H "x-orchestrator-key: orc_YOUR_ORCHESTRATOR_KEY"
```

Deactivation preserves the endpoint URL and signing secret. Registering the same URL again is rejected, even while it is inactive; reactivate it instead. To use a different URL, deactivate the old endpoint and register the new one.

## Current webhook events

These are the events for current Secure Payment and orchestrator integrations. The platform's Client ID receives the events shown below for Secure Payments created with that Client ID.

| Event                            | Platform's Client ID | Linked orchestrator | Notes                                                                                          |
| -------------------------------- | -------------------- | ------------------- | ---------------------------------------------------------------------------------------------- |
| `client_id.linked`               | No                   | Yes                 | Sent after hosted onboarding completes. Direct API links do not emit this event.               |
| `payment.confirmed`              | Yes                  | Yes                 | Both receive it when the linked orchestrator created the Secure Payment for the platform.      |
| `payment.failed`                 | Yes                  | No                  | Payment execution fails.                                                                       |
| `kyt.screening.completed`        | Yes                  | Yes                 | Sent after a definitive `approved` or `rejected` result. Provider errors do not emit it.       |
| `secure_payment.user_event`      | Yes                  | Yes                 | Best-effort payer activity from the Secure Payment Page. Do not use it as a settlement signal. |
| `secure_payment.access_rejected` | Yes                  | No                  | A wallet outside an incoming payment's payer-wallet allowlist tries to access or pay it.       |

The `userEvent` field distinguishes the 3 funnel steps:

| `userEvent`                  | Meaning                                                                                                                             |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `wallet_connected`           | The payer successfully connected a wallet on the secure payment page                                                                |
| `payment_sent_to_wallet`     | The payment transaction was handed to the payer's wallet for signature                                                              |
| `payment_approved_in_wallet` | The payer approved/signed the payment in their wallet. `properties` includes the submission id (e.g. tx hash / user-operation hash) |

<Note>
  `payment.confirmed` is the settlement signal. `secure_payment.user_event` is browser-reported activity: navigation, network failures, or browser extensions can prevent Request Network from receiving it. Its absence does not prove that the payer did not take that step.

  When the Secure Payment Page includes wallet information in `properties`, it uses `wallet_address_hashed` rather than a raw wallet address.
</Note>

### Payer-wallet access rejections

`secure_payment.access_rejected` is generated server-side when a wallet that is not on an incoming payment's `allowedPayerAddresses` allowlist tries to access or pay it. It is not emitted for KYT decisions. It goes to the payment's Client ID endpoints, including any existing platform-wide endpoint, not to the orchestrator. See [Restrict payer wallets](/use-cases/restrict-payer-wallets) to configure the allowlist.

Repeated attempts by the same wallet on the same payment are normally suppressed for 10 minutes. If every configured webhook endpoint fails, the next access attempt can trigger another notification.

## Security Implementation

### Signature Verification

Every webhook includes an HMAC SHA-256 signature in the `x-request-network-signature` header:

```javascript theme={null}
import crypto from "node:crypto";

function verifyWebhookSignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  } catch {
    return false;
  }
}

// Usage in your webhook handler
app.post("/webhook", (req, res) => {
  const signature = req.headers["x-request-network-signature"];
  
  if (!verifyWebhookSignature(req.rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "Invalid signature" });
  }
  
  // Parse JSON after verification
  const body = JSON.parse(req.rawBody.toString("utf8"));
  
  // Process webhook...
  res.status(200).json({ success: true });
});
```

### Security Requirements

* **HTTPS only:** Production webhooks require HTTPS endpoints
* **Always verify signatures:** Never process unverified webhook requests
* **Keep secrets secure:** Store signing secrets as environment variables
* **Return 2xx for success:** Any 2xx status code confirms successful processing

## Request Headers

Each webhook request includes these headers:

| Header                          | Description                 | Example                      |
| ------------------------------- | --------------------------- | ---------------------------- |
| `x-request-network-signature`   | HMAC SHA-256 signature      | `a1b2c3d4e5f6...`            |
| `x-request-network-delivery`    | Unique delivery ID (ULID)   | `01ARZ3NDEKTSV4RRFFQ69G5FAV` |
| `x-request-network-retry-count` | Current retry attempt (0-3) | `0`                          |
| `x-request-network-test`        | Present for test webhooks   | `true`                       |
| `content-type`                  | Always JSON                 | `application/json`           |

## Retry Logic

### Automatic Retries

* **Max attempts:** 3 retries (4 total attempts)
* **Retry delays:** 1s, 5s, 15s
* **Trigger conditions:** Non-2xx response codes, timeouts, connection errors
* **Timeout:** 5 seconds per request

### Response Handling

```javascript theme={null}
// ✅ Success - no retry
res.status(200).json({ success: true });
res.status(201).json({ created: true });

// ❌ Error - triggers retry
res.status(401).json({ error: "Unauthorized" });
res.status(404).json({ error: "Resource not found" });
res.status(500).json({ error: "Internal server error" });
```

### Error Logging

Request API logs all webhook delivery failures with:

* Endpoint URL
* Attempt number
* Error details
* Final failure after all retries

## Payload identity and examples

For a Secure Payment created with a Client ID, payloads include `clientId`. When it was created through an orchestrator, payloads also include `orchestratorId`.

`orchestratorId` is the orchestrator recorded when the Secure Payment was created. Linking, unlinking, or relinking that Client ID later does not change historical payment events. Use `requestId`, `paymentToken`, or `securePaymentToken` to correlate an event with your records.

Payment events include an `explorer` field linking to [Request Scan](https://scan.request.network) when one is available. `requestId` and `requestID` identify the request, and `paymentReference` is its short unique reference.

`payerAddress` is the resolved payer wallet and `payerEoaAddress` is the payer's connected wallet. They can differ when a smart account is used. Both are `null` when unavailable and are included on `payment.confirmed` and `payment.partial` events.

### Client ID linked

`client_id.linked` is sent to an orchestrator after a platform completes hosted onboarding from a link intent.

```json theme={null}
{
  "event": "client_id.linked",
  "clientId": "cli_PLATFORM_CLIENT_ID",
  "orchestratorId": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
  "linkId": "01ARZ3NDEKTSV4RRFFQ69G5FAX",
  "intentId": "01ARZ3NDEKTSV4RRFFQ69G5FAY",
  "externalId": "merchant_123",
  "destinationId": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e@eip155:8453#B4FD67BB:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "destinationWalletAddress": "0x742d35cc6634c0532925a3b844bc454e4438f44e",
  "chain": "base",
  "currency": "USDC",
  "timestamp": "2026-08-14T10:00:00.000Z"
}
```

Use `intentId` or `externalId` to match this event to your onboarding record. Use `linkId` or `intentId` to identify a repeated delivery.

### Payment Confirmed

```json theme={null}
{
  "event": "payment.confirmed",
  "requestId": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "requestID": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "paymentReference": "0x2c3366941274c34c",
  "clientId": "cli_PLATFORM_CLIENT_ID",
  "orchestratorId": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
  "explorer": "https://scan.request.network/request/0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "amount": "100.0",
  "totalAmountPaid": "100.0",
  "expectedAmount": "100.0",
  "timestamp": "2025-10-03T14:30:00Z",
  "txHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
  "payerAddress": "0x92Fc3406Fc6BB7A76aC63b2E8b9d02b1B9C3e4d5",
  "payerEoaAddress": "0x7A1F20C4D58E9B0A3C6D4E2F1B8A5C7D9E0F1234",
  "network": "ethereum",
  "currency": "USDC",
  "paymentCurrency": "USDC",
  "isCryptoToFiat": false,
  "subStatus": "",
  "paymentProcessor": "request-network",
  "fees": [
    {
      "type": "network",
      "amount": "0.02",
      "currency": "ETH"
    }
  ]
}
```

### Payment Processing

```json theme={null}
{
  "event": "payment.processing",
  "requestId": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "requestID": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "paymentReference": "0x2c3366941274c34c",
  "offrampId": "offramp_test123456789",
  "timestamp": "2025-10-03T14:35:00Z",
  "subStatus": "ongoing_checks",
  "paymentProcessor": "request-tech",
  "rawPayload": {
    "status": "ongoing_checks",
    "providerId": "provider_test123"
  }
}
```

### Payment Partial

```json theme={null}
{
  "event": "payment.partial",
  "requestId": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "requestID": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "paymentReference": "0x2c3366941274c34c",
  "explorer": "https://scan.request.network/request/0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "amount": "50.0",
  "totalAmountPaid": "50.0",
  "expectedAmount": "100.0",
  "timestamp": "2025-10-03T14:30:00Z",
  "txHash": "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
  "payerAddress": "0x92Fc3406Fc6BB7A76aC63b2E8b9d02b1B9C3e4d5",
  "payerEoaAddress": "0x7A1F20C4D58E9B0A3C6D4E2F1B8A5C7D9E0F1234",
  "network": "ethereum",
  "currency": "USDC",
  "paymentCurrency": "USDC",
  "isCryptoToFiat": false,
  "subStatus": "",
  "paymentProcessor": "request-network",
  "fees": []
}
```

### Payment Failed

```json theme={null}
{
  "event": "payment.failed",
  "requestId": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "requestID": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "paymentReference": "0x2c3366941274c34c",
  "subStatus": "insufficient_funds",
  "paymentProcessor": "request-network"
}
```

### Compliance Updated

```json theme={null}
{
  "event": "compliance.updated",
  "clientUserId": "user_test123456789",
  "kycStatus": "approved",
  "agreementStatus": "completed",
  "isCompliant": true,
  "timestamp": "2025-10-03T14:30:00Z",
  "rawPayload": {
    "verificationLevel": "full",
    "documents": "verified"
  }
}
```

### KYT Screening Completed

`kyt.screening.completed` is sent after a Secure Payment reaches an `approved` or `rejected` screening result.

```json theme={null}
{
  "event": "kyt.screening.completed",
  "paymentToken": "01KYM5CZ51K0N1KJ4F8S73BE3N",
  "clientId": "cli_PLATFORM_CLIENT_ID",
  "orchestratorId": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
  "walletAddress": "0x2e2e5c79f571ef1658d4c2d3684a1fe97dd30570",
  "eoaAddress": "0x2e2e5c79f571ef1658d4c2d3684a1fe97dd30570",
  "smartAccountAddress": null,
  "status": "approved",
  "provider": "hypernative",
  "policyId": "11111111-1111-4111-8111-811111111111",
  "timestamp": "2026-08-14T10:00:00.000Z"
}
```

`provider` is the provider that produced the result. `policyId` is `null` when the provider account default was used.

### Secure Payment User Event

```json theme={null}
{
  "event": "secure_payment.user_event",
  "userEvent": "payment_approved_in_wallet",
  "securePaymentToken": "spt_3fk29ax7...",
  "requestId": "01JD3E6JD46KY4KKV7X9V0MZ7W",
  "requestIds": ["01JD3E6JD46KY4KKV7X9V0MZ7W"],
  "clientId": "cli_PLATFORM_CLIENT_ID",
  "orchestratorId": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
  "occurredAt": "2026-08-05T14:03:21.512Z",
  "timestamp": "2026-08-05T14:03:22.104Z",
  "properties": {
    "wallet_provider": "metamask",
    "payment_submission_id": "0x6a4f...e21b",
    "payment_submission_id_type": "evm_tx_hash",
    "selected_source_chain": "base",
    "payment_type": "single"
  }
}
```

### Secure Payment Access Rejected

```json theme={null}
{
  "event": "secure_payment.access_rejected",
  "requestId": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "clientId": "cli_PLATFORM_CLIENT_ID",
  "orchestratorId": "01ARZ3NDEKTSV4RRFFQ69G5FAW",
  "attemptedPayerWalletAddress": "0x2e2e5c79f571ef1658d4c2d3684a1fe97dd30570",
  "timestamp": "2026-08-10T10:05:00.000Z"
}
```

| Field                         | Description                                                                                         |
| ----------------------------- | --------------------------------------------------------------------------------------------------- |
| `requestId`                   | The request the wallet tried to access.                                                             |
| `attemptedPayerWalletAddress` | The rejected wallet address. EVM addresses are lowercased; TRON addresses keep their original case. |
| `timestamp`                   | When Request Network emitted the event.                                                             |

Use `POST /v1/webhook/test` with `{ "eventType": "secure_payment.access_rejected" }` to test this event without a rejected access attempt.

## Legacy integrations

These events remain available for existing API integrations. They do not apply to current Dashboard, Secure Payment Page, or orchestrator workflows.

| Event                    | Legacy flow                        |
| ------------------------ | ---------------------------------- |
| `payment.partial`        | Partial-payment flow               |
| `payment.refunded`       | Existing refund flows              |
| `payment.processing`     | Crypto-to-fiat processing          |
| `compliance.updated`     | Crypto-to-fiat compliance updates  |
| `payment_detail.updated` | Crypto-to-fiat bank-detail updates |
| `request.recurring`      | Recurring requests                 |

## Implementation Examples

For a complete working example, see [Webhook reconciliation](/use-cases/webhook-reconciliation) which implements webhook handling for payment notifications.

<Tabs>
  <Tab title="Express.js">
    ```javascript theme={null}
    import express from "express";
    import crypto from "node:crypto";

    const app = express();
    const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

    // Use raw body parser to capture exact request bytes for signature verification
    app.use(
      express.raw({
        type: "application/json",
        verify: (req, _res, buf) => {
          req.rawBody = buf;
        },
      })
    );

    app.post("/webhook/payment", async (req, res) => {
      try {
        // Verify signature against raw body
        const signature = req.headers["x-request-network-signature"];
        const deliveryId = req.headers["x-request-network-delivery"];
        const rawBody = req.rawBody;

        const expectedSignature = crypto
          .createHmac("sha256", WEBHOOK_SECRET)
          .update(rawBody)
          .digest("hex");

        if (!signature || !deliveryId) {
          return res.status(400).json({ error: "Missing webhook headers" });
        }

        if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
          return res.status(401).json({ error: "Invalid signature" });
        }

        // Parse JSON only after verifying signature
        const body = JSON.parse(rawBody.toString("utf8"));
        const isTest = req.headers["x-request-network-test"] === "true";

        if (isTest) {
          console.log("Received test webhook");
        }

        // Process webhook based on event type
        const { event, requestId } = body;
        
        switch (event) {
          case "payment.confirmed":
            await handlePaymentConfirmed(body);
            break;
          case "payment.processing":
            await handlePaymentProcessing(body);
            break;
          case "compliance.updated":
            await handleComplianceUpdate(body);
            break;
          case "secure_payment.access_rejected":
            await recordPayerWalletRejection(
              requestId,
              body.attemptedPayerWalletAddress,
              deliveryId,
            );
            break;
          default:
            console.log(`Unhandled event: ${event}`);
        }

        return res.status(200).json({ success: true });
        
      } catch (error) {
        console.error("Webhook processing error:", error);
        return res.status(500).json({ error: "Processing failed" });
      }
    });
    ```
  </Tab>

  <Tab title="Next.js">
    ```javascript theme={null}
    // app/api/webhook/route.ts
    import crypto from "node:crypto";
    import { NextResponse } from "next/server";

    export async function POST(request: Request) {
      try {
        // Read raw body for signature verification
        const rawBody = await request.text();
        const signature = request.headers.get("x-request-network-signature");
        const expectedSignature = crypto
          .createHmac("sha256", process.env.WEBHOOK_SECRET!)
          .update(rawBody)
          .digest("hex");

        if (!signature || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
          return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
        }

        // Parse JSON after verifying signature
        const body = JSON.parse(rawBody);

        // Process webhook
        const { event, requestId } = body;
        
        // Your business logic here
        await processWebhookEvent(event, body);

        return NextResponse.json({ success: true }, { status: 200 });
        
      } catch (error) {
        console.error("Webhook error:", error);
        return NextResponse.json(
          { error: "Internal server error" }, 
          { status: 500 }
        );
      }
    }
    ```
  </Tab>
</Tabs>

## Testing

### Test deliveries

Fire a test webhook from the Auth API:

```bash theme={null}
curl -X POST "https://auth.request.network/v1/webhook/test" \
  -H "Content-Type: application/json" \
  -H "x-client-id: YOUR_CLIENT_ID" \
  -d '{ "eventType": "payment.confirmed" }'
```

Or call it interactively from the [Auth API Scalar docs](https://auth.request.network/open-api/#tag/webhook/POST/v1/webhook/test).

Test deliveries arrive at all active webhooks for that Client ID and include the `x-request-network-test: true` header so handlers can branch on test vs real.

### Test Webhook Identification

Test webhooks include the `x-request-network-test: true` header:

```javascript theme={null}
app.post("/webhook", (req, res) => {
  const isTest = req.headers["x-request-network-test"] === "true";
  
  if (isTest) {
    console.log("Received test webhook");
    // Handle test scenario
  }
  
  // Process normally...
});
```

## Best Practices

### Error Handling

* **Implement idempotency:** Use delivery IDs to prevent duplicate processing
* **Graceful degradation:** Handle unknown event types without errors

### Performance

* **Timeout management:** Complete processing within 5 seconds

## Troubleshooting

### Common Issues

**Signature verification fails:**

* Check your signing secret matches the value returned by `POST /v1/webhook` at creation
* Ensure you're using the raw request body for signature calculation
* Verify HMAC SHA-256 implementation

**Webhooks not received:**

* Confirm endpoint URL is accessible via HTTPS
* Verify endpoint returns 2xx status codes
* Confirm the webhook is `active` via `GET /v1/webhook` (toggle with `PUT /v1/webhook/:id`)

### Debugging Tips

* Use ngrok request inspector to see raw webhook data
* Monitor retry counts in headers to identify issues
* Fire test deliveries via `POST /v1/webhook/test`

## Related Documentation

<CardGroup cols={2}>
  <Card title="Webhooks & Events" href="/api-features/webhooks-events">
    High-level webhook concepts and workflow
  </Card>

  <Card title="Webhook reconciliation" href="/use-cases/webhook-reconciliation">
    Complete webhook implementation example
  </Card>

  <Card title="Authentication" href="/api-reference/authentication">
    API credential setup and webhook security
  </Card>

  <Card title="Request Dashboard" href="https://dashboard.request.network" icon="browser">
    Manage Client IDs, payment destinations, and webhooks
  </Card>
</CardGroup>
