> ## Documentation Index
> Fetch the complete documentation index at: https://requestnetwork-fix-req-350-remove-unused-webhook-events.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Complete webhook implementation guide with event types, security, and retry configuration

## Overview

Webhooks deliver real-time notifications when payment and request events occur. Configure your endpoints to receive HMAC-signed POST requests with automatic retry logic and comprehensive event data.

## Webhook Configuration

Manage webhooks in the [Dashboard](https://dashboard.request.network) or programmatically through the Auth API at `auth.request.network`. Each webhook is scoped to the Client ID that creates it; events for any payment link or request created with that Client ID are delivered to that webhook.

### Create a webhook

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

**Response (201 Created):**

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

<Warning>
  The `secret` is only returned once at creation. Store it securely — you cannot retrieve it again. Use HTTPS in production. `localhost` URLs are accepted for local testing.
</Warning>

### Manage webhooks

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 that's shared across all `*.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
```

## Event Types

<Info>
  See [Payload Examples](#payload-examples) below for detailed webhook structures.
</Info>

### Payment Events (core)

| Event               | Description                          | Context                                        | Primary Use                                      |
| ------------------- | ------------------------------------ | ---------------------------------------------- | ------------------------------------------------ |
| `payment.confirmed` | Payment fully completed and settled  | After blockchain confirmation                  | Complete fulfillment, release goods              |
| `payment.partial`   | Partial payment received for request | Installments, partial orders                   | Update balance, allow additional payments        |
| `payment.failed`    | Payment execution failed             | Recurring payments, cross-chain transfers      | Notify failure, retry logic, pause subscriptions |
| `payment.refunded`  | Payment has been refunded to payer   | Cross-chain payment failures, refund scenarios | Update order status, notify customer             |

### Processing Events

| Event                | Description                        | Context                                                                                                             | Primary Use                                    |
| -------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `payment.processing` | Crypto-to-fiat payment in progress | **subStatus values:** initiated, pending\_internal\_assessment, ongoing\_checks, sending\_fiat, fiat\_sent, bounced | Track crypto-to-fiat payment status, update UI |

### Request Events

| Event               | Description                     | Context                                   | Primary Use                                |
| ------------------- | ------------------------------- | ----------------------------------------- | ------------------------------------------ |
| `request.recurring` | New recurring request generated | Subscription renewals, scheduled payments | Send renewal notifications, update billing |

### Compliance Events

| Event                    | Description                              | Context                                                                                                                                                              | Primary Use                            |
| ------------------------ | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| `compliance.updated`     | KYC or agreement status changed          | **kycStatus values:** not\_started, pending, approved, rejected, retry\_required<br />**agreementStatus values:** not\_started, pending, completed, rejected, failed | Update user permissions, notify status |
| `payment_detail.updated` | Bank account verification status updated | States: approved, failed, pending                                                                                                                                    | Enable fiat payments, update profiles  |

### Secure Payment Events

| Event                            | Description                                                                      | Context                                                                                           | Primary Use                                           |
| -------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `secure_payment.user_event`      | Payer progressed through a step of the Secure Payment Page                       | **userEvent values:** wallet\_connected, payment\_sent\_to\_wallet, payment\_approved\_in\_wallet | Real-time payer-funnel visibility, drop-off analytics |
| `secure_payment.access_rejected` | A wallet not on a payment's payer-wallet allowlist attempted to access or pay it | Incoming Secure Payments with `allowedPayerAddresses`                                             | Monitor rejected payer attempts                       |

Sent to the same registered webhook endpoints as every other event — same Client ID scoping, `x-request-network-signature` HMAC verification, delivery headers, timeout, and 1s/5s/15s retry semantics described elsewhere on this page.

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>
  `securePaymentToken` is the platform's correlation key, returned when the secure payment was created. `requestId` is present only when exactly one request is linked to the secure payment (see `requestIds` for the full list). `timestamp` is server-stamped on receipt. `occurredAt` and `properties` are **client-reported telemetry from the payer's browser** — useful for analytics, but not authoritative.

  `secure_payment.user_event` is best-effort browser telemetry. Navigation, network errors, or browser extensions can prevent the API from receiving it. Webhook retries begin only after the API accepts the event. Do not treat an absent event as evidence that the payer did not take the step; use `payment.confirmed` for settlement and reconciliation.

  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. See [Restrict payer wallets](/use-cases/restrict-payer-wallets) to configure the allowlist.

The event is sent to the payment's platform-wide and Client ID webhooks, not to an orchestrator webhook. 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 Examples

All payment events include an `explorer` field linking to [Request Scan](https://scan.request.network) for transaction details.

**Common Fields:**

* `requestId` / `requestID`: Unique identifier for the payment request
* `paymentReference`: Short reference, also unique to a request, used to link payments to the request
* `timestamp`: ISO 8601 formatted event timestamp
* `paymentProcessor`: Either `request-network` (crypto) or `request-tech` (fiat)
* `payerAddress`: Resolved payer wallet — the on-chain sender for plain direct payments, or the resolved payer for recurring and intent-based flows (Secure Payment Page, LiFi, Safe, ERC-4337, multicall). `null` when it cannot be determined. Included on `payment.confirmed` and `payment.partial` events.
* `payerEoaAddress`: The payer's connected wallet address. It can differ from `payerAddress` when a smart account is used. `null` when unavailable. Included on `payment.confirmed` and `payment.partial` events.

### Payment Confirmed

```json theme={null}
{
  "event": "payment.confirmed",
  "requestId": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "requestID": "0151b394e3c482c5aebaa04eb04508a8db70595470760293f1b258ed96d1fafa93",
  "paymentReference": "0x2c3366941274c34c",
  "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"
  }
}
```

### Secure Payment User Event

```json theme={null}
{
  "event": "secure_payment.user_event",
  "userEvent": "payment_approved_in_wallet",
  "securePaymentToken": "spt_3fk29ax7...",
  "requestId": "01JD3E6JD46KY4KKV7X9V0MZ7W",
  "requestIds": ["01JD3E6JD46KY4KKV7X9V0MZ7W"],
  "orchestratorId": "orch_12345",
  "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",
  "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.

## 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>
