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.
- Create a webhook endpoint with your HTTPS URL and the events you want to subscribe to.
- Store the
secretreturned in the response — it's only shown once and is used to verify webhook signatures. - 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
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
{
"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
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
{
"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.