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

# 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

Webhooks are managed **programmatically** via the Auth API at `auth.request.network` — there is no Dashboard UI for webhook CRUD. 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             |

### Payment Events (Client ID-scoped)

Emitted **in addition to** the core events when the originating request was created with a Client ID. Payload includes extra `clientId` and `origin` fields.

| Event                         | Description                                        |
| ----------------------------- | -------------------------------------------------- |
| `payment.confirmed.client_id` | Same as `payment.confirmed`, scoped to a Client ID |
| `payment.partial.client_id`   | Same as `payment.partial`, scoped to a Client ID   |

### Payment Events (Checkout / Secure Payment-scoped)

Emitted **in addition to** the core events when the request was created via a Secure Payment / checkout flow.

| Event                        | Description                                                         |
| ---------------------------- | ------------------------------------------------------------------- |
| `payment.confirmed.checkout` | Same as `payment.confirmed`, originating from a Secure Payment link |
| `payment.partial.checkout`   | Same as `payment.partial`, originating from a Secure Payment link   |

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

## 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 (and their `.client_id` / `.checkout` variants).

### 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",
  "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",
  "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"
  }
}
```

## 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 rawBody = req.rawBody;

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

        if (!signature || !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;
          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 and payment destinations (webhooks are managed via the Auth API above)
  </Card>
</CardGroup>
