# Webhooks

Webhooks notify your backend when payment, withdrawal, and payout-routing state changes. Use webhooks for real-time updates, then fetch the canonical PayChain resource before releasing value.

{% hint style="info" %}
**Source of truth:** A webhook tells you an event happened. The invoice, withdrawal, or routing record remains the canonical state for fulfillment and reconciliation.
{% endhint %}

### What webhooks are for

Use webhooks to:

* Mark an order as paid.
* Release a digital good or service.
* Update a customer deposit ledger.
* Track withdrawal progress.
* Track automated payout routing progress.
* Alert operators when payout routing fails or a withdrawal is blocked.

### Signature verification

Verify every webhook using the raw request body.

```http
X-Webhook-Signature: ...
X-Webhook-Timestamp: ...
X-Webhook-ID: ...
```

{% hint style="warning" %}
**Raw body note:** Do not parse JSON and then re-stringify it before verification. Signature verification must use the exact raw bytes PayChain sent.
{% endhint %}

#### Express raw-body example

```ts
import express from 'express';
import { verifyWebhookSignature } from '@paychainhq/sdk';

const app = express();

app.post('/paychain/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const valid = verifyWebhookSignature({
    rawBody: req.body,
    signature: req.header('X-Webhook-Signature'),
    timestamp: req.header('X-Webhook-Timestamp'),
    secret: process.env.PAYCHAIN_WEBHOOK_SECRET!,
    toleranceSeconds: 300
  });

  if (!valid) {
    return res.status(400).send('invalid signature');
  }

  const event = JSON.parse(req.body.toString('utf8'));

  // Store event.id and process idempotently.
  // Fetch the canonical invoice, withdrawal, or routing record before fulfillment.

  return res.sendStatus(200);
});
```

#### Next.js route handler example

```ts
import { NextResponse } from 'next/server';
import { verifyWebhookSignature } from '@paychainhq/sdk';

export async function POST(request: Request) {
  const rawBody = await request.text();

  const valid = verifyWebhookSignature({
    rawBody,
    signature: request.headers.get('X-Webhook-Signature'),
    timestamp: request.headers.get('X-Webhook-Timestamp'),
    secret: process.env.PAYCHAIN_WEBHOOK_SECRET!,
    toleranceSeconds: 300
  });

  if (!valid) {
    return new NextResponse('invalid signature', { status: 400 });
  }

  const event = JSON.parse(rawBody);

  // Store event.id and process idempotently.
  // Fetch the canonical invoice, withdrawal, or routing record before fulfillment.

  return NextResponse.json({ received: true });
}
```

### Recommended handler flow

1. Receive the webhook request.
2. Read the raw body.
3. Verify the signature and timestamp.
4. Check idempotency using the webhook event ID.
5. Fetch the invoice, withdrawal, or routing record from PayChain.
6. Apply your business action once.
7. Return a `2xx` response quickly.

### Common event families

| Event family               | Meaning                                     |
| -------------------------- | ------------------------------------------- |
| `invoice.*`                | Invoice lifecycle changed.                  |
| `withdrawal.*`             | Withdrawal lifecycle changed.               |
| `invoice.payout_routing.*` | Automated payout routing lifecycle changed. |

Common examples:

* `invoice.paid`
* `invoice.overpaid`
* `invoice.failed`
* `withdrawal.created`
* `withdrawal.processing`
* `withdrawal.completed`
* `withdrawal.failed`
* `invoice.payout_routing.started`
* `invoice.payout_routing.completed`
* `invoice.payout_routing.failed`

### Example payloads

Webhook payloads include an event ID, event type, environment, business ID, timestamp, and a `data` object for the resource that changed. The exact schema may vary by event, so always use the event `type` and then fetch the canonical resource before fulfilling an order or marking a payout complete.

#### Invoice paid

```json
{
  "id": "evt_01HX...",
  "type": "invoice.paid",
  "environment": "live",
  "businessId": "biz_123",
  "createdAt": "2026-05-20T12:30:00.000Z",
  "data": {
    "invoiceId": "inv_123",
    "status": "paid",
    "customerId": "cus_123",
    "amount": "100.00",
    "paidAmount": "100.00",
    "token": "USDC",
    "chain": "evm",
    "networkId": "base-mainnet",
    "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "metadata": {
      "orderId": "order_123"
    }
  }
}
```

#### Withdrawal completed

```json
{
  "id": "evt_01HY...",
  "type": "withdrawal.completed",
  "environment": "live",
  "businessId": "biz_123",
  "createdAt": "2026-05-20T12:36:00.000Z",
  "data": {
    "withdrawalId": "wd_123",
    "status": "completed",
    "amount": "50.00",
    "token": "USDC",
    "chain": "evm",
    "networkId": "base-mainnet",
    "destinationAddress": "0x0000000000000000000000000000000000000000",
    "clientReference": "payout_123",
    "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
  }
}
```

#### Payout routing failed

```json
{
  "id": "evt_01HZ...",
  "type": "invoice.payout_routing.failed",
  "environment": "live",
  "businessId": "biz_123",
  "createdAt": "2026-05-20T12:40:00.000Z",
  "data": {
    "invoiceId": "inv_123",
    "routingId": "route_run_123",
    "status": "failed",
    "failureReason": "Payout quota exceeded before all recipient legs could be created.",
    "payoutRouteId": "route_123"
  }
}
```

{% hint style="warning" %}
**Fulfillment note:** Do not fulfill from the webhook payload alone. Verify the signature, store the event ID, fetch the invoice or withdrawal from PayChain, then apply your business action once.
{% endhint %}

### Retry behavior

If your endpoint does not return a successful response, PayChain retries delivery according to the configured retry policy. Repeated failures eventually move the delivery to a failed state.

Return a `2xx` response only after your system has accepted the event for processing. If you need more time, store the event and process it asynchronously; do not hold the webhook request open for long-running fulfillment, payout, or customer-notification work.

{% hint style="info" %}
**Manual recovery:** Failed webhook events can be retried or replayed from the webhook event APIs where supported. A replay creates a fresh delivery event.
{% endhint %}

### Endpoint management

Use the webhook API to:

* Get webhook configuration.
* Update the webhook URL or enabled state.
* Rotate the webhook signing secret.
* Send a test webhook.
* List webhook events.
* Retry or replay failed events.

See OpenAPI reference for the exact endpoint schemas.

### Common mistakes

* Treating webhook delivery as fulfillment without fetching the canonical resource.
* Not storing processed webhook IDs.
* Verifying against parsed JSON instead of raw body.
* Taking too long before responding.
* Using the same webhook URL and secret for sandbox and live.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.paychainhq.io/paychainhq-documentation-page/developer-quickstart/webhooks.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
