# Webhooks

Webhooks notify your application in real-time when events occur in Chain — payments completing, card authorizations, business verification updates, and more.

## Setup

You can manage webhook endpoints via the API or from the [Chain Dashboard](https://app.chain.com/).

1. Create a webhook endpoint with your HTTPS URL and the events you want to subscribe to.
2. Store the `secret` returned in the response — it's only shown once and is used to verify webhook signatures.
3. Implement signature verification in your endpoint handler (see below).

## Manage Webhook Endpoints

Create, list, and delete webhook endpoints programmatically.

**POST/v1/webhooks**

| Parameter | Type | Description |
| --- | --- | --- |
| `url` required | `string` | Endpoint URL (must be HTTPS). |
| `events` required | `string[]` | Array of event types to subscribe to. |
| `description` | `string` | Optional description (max 200 characters). |

**Create webhook endpoint**

```bash
curl -X POST https://api.chain.com/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/chain",
    "events": ["funding.completed", "payout.completed", "card.authorization.request"],
    "description": "Production webhook handler"
  }'
```

**Response**

```json
{
  "id": "wh_abc123",
  "url": "https://example.com/webhooks/chain",
  "events": ["funding.completed", "payout.completed", "card.authorization.request"],
  "description": "Production webhook handler",
  "status": "active",
  "secret": "whsec_abc123...",
  "created_at": "2026-01-15T10:00:00Z"
}
```

Store the secret

The `secret` is only returned when the endpoint is created. Store it securely — you'll need it to verify webhook signatures.

**GET/v1/webhooks**

List all webhook endpoints. Secrets are masked in the response.

**DELETE/v1/webhooks?endpointId=wh_abc123**

Delete a webhook endpoint. Pass the endpoint ID as a query parameter.

## Signature Verification

Every webhook includes a `X-Chain-Signature` header containing an HMAC-SHA256 signature. Always verify this signature to confirm the event came from Chain.

**Node.js**

```javascript
import crypto from 'crypto';

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// In your route handler:
app.post('/webhooks/chain', (req, res) => {
  const signature = req.headers['x-chain-signature'];
  const isValid = verifyWebhook(
    JSON.stringify(req.body),
    signature,
    process.env.CHAIN_WEBHOOK_SECRET
  );

if (!isValid) {
    return res.status(401).send('Invalid signature');
  }

// Process event...
  res.status(200).send('OK');
});
```

## Event Types

### Payments

| Event | Description |
| --- | --- |
| `funding.initiated` | A funding request has been created. |
| `funding.completed` | Funds have been received and converted. |
| `funding.failed` | A funding request has failed. |
| `payout.initiated` | A payout has been created. |
| `payout.completed` | Payout has been delivered to the recipient. |
| `payout.failed` | A payout has failed. |

### Cards

| Event | Description |
| --- | --- |
| `card.created` | A new card has been issued. |
| `card.authorization.request` | Real-time authorization request (respond within 2s). |
| `card.transaction.completed` | A card transaction has settled. |
| `card.frozen` | A card has been frozen. |
| `card.cancelled` | A card has been cancelled. |

### Business & Compliance

| Event | Description |
| --- | --- |
| `business.verification.approved` | Business KYB verification approved. |
| `business.verification.rejected` | Business KYB verification rejected. |
| `compliance.screening.flagged` | A compliance screening flagged an issue. |
| `report.ready` | A generated report is ready for download. |

## Payload Format

All webhook payloads follow a consistent structure.

**Webhook payload**

```json
{
  "id": "evt_abc123",
  "type": "funding.completed",
  "created_at": "2026-01-15T10:30:00Z",
  "data": {
    "id": "fund_123abc",
    "status": "completed",
    "amount": 10000,
    "currency": "USD",
    "target_amount": 9985.50,
    "target_currency": "USDC"
  }
}
```

## Retry Policy

Idempotency

Your webhook handler should be idempotent. Chain may deliver the same event more than once. Use the `id` field to deduplicate.

| Attempt | Delay |
| --- | --- |
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 24 hours |

After 5 failed attempts, the webhook is marked as failed and the endpoint may be disabled. You can retry failed webhooks from the Dashboard.
