Webhooks
Webhooks allow you to receive real-time notifications about payment events in your account. When an event occurs (like an on-chain payment confirmation or subscription renewal), we send an HTTP POST request to your configured endpoint.
Webhooks are essential for keeping your system in sync with on-chain payment events, especially since blockchain transactions require confirmation time.
How It Works
- Configure your endpoint - Set up a publicly accessible URL to receive webhook events
- Receive events - We POST event data to your endpoint when events occur
- Process and respond - Your server processes the event and returns a 2xx status code
- Retry on failure - Failed deliveries are automatically retried
Setting Up Webhooks
Configure your webhook endpoint in your dashboard settings. You’ll need to provide:
- Endpoint URL: A publicly accessible HTTPS URL
- Events to subscribe: Select which events you want to receive
- Secret key: Used to verify webhook signatures
Webhook Events
Payment Events
| Event | Description |
|---|---|
payment.succeeded | Payment confirmed on-chain (direct mode) |
payment.failed | Payment attempt failed |
payment.authorized | Payment received, awaiting capture (two-step mode) |
payment.captured | Funds captured to settlement wallet (two-step mode) |
payment.refunded | Payment refunded to customer (two-step mode) |
Subscription Events
| Event | Description |
|---|---|
subscription.created | New subscription was created |
subscription.paused | Subscription was paused |
subscription.past_due | Renewal payment attempt failed |
subscription.cancelled | Subscription was cancelled |
Event Payload Structure
All webhook events follow a consistent structure:
{
"id": "evt_1234567890abcdef",
"object": "event",
"type": "payment.succeeded",
"created_at": "2024-01-15T12:30:00Z",
"data": {
"object": {
"id": "pay_0987654321fedcba",
"object": "payment",
"amount": 2999,
"currency": "USD",
"status": "succeeded",
"stablecoin": "USDC",
"network": "ethereum",
"tx_hash": "0x8a9c67b2d1e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9"
}
}
}{
"id": "evt_abcdef1234567890",
"object": "event",
"type": "payment.captured",
"created_at": "2024-01-15T14:00:00Z",
"data": {
"object": {
"id": "pay_0987654321fedcba",
"object": "payment",
"amount": 5000,
"currency": "USD",
"status": "captured",
"payment_mode": "two_step",
"stablecoin": "USDC",
"network": "ethereum",
"tx_hash": "0x8a9c67b2d1e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9",
"created_at": "2024-01-15T12:30:00Z",
"captured_at": "2024-01-15T14:00:00Z"
}
}
}{
"id": "evt_fedcba0987654321",
"object": "event",
"type": "payment.refunded",
"created_at": "2024-01-20T15:30:00Z",
"data": {
"object": {
"id": "pay_0987654321fedcba",
"object": "payment",
"amount": 5000,
"currency": "USD",
"status": "refunded",
"payment_mode": "two_step",
"stablecoin": "USDC",
"network": "ethereum",
"created_at": "2024-01-15T12:30:00Z",
"refunded_at": "2024-01-20T15:30:00Z"
}
}
}Verifying Webhook Signatures
Every webhook request includes a signature in the X-Signature header. Verify this signature to ensure the request came from our servers.
Signature Verification
The signature is computed using HMAC-SHA256 with your webhook secret:
import crypto from 'crypto';
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler
app.post('/webhooks/payments', (req, res) => {
const signature = req.headers['x-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = req.body;
console.log('Received event:', event.type);
res.status(200).send('OK');
});Always verify webhook signatures in production to prevent malicious requests.
Handling Webhooks
Best Practices
- Return quickly: Respond with a 2xx status code within 30 seconds
- Process asynchronously: Queue events for background processing if needed
- Handle duplicates: Events may be delivered more than once; use
event.idto deduplicate - Verify signatures: Always verify the webhook signature before processing
Example Handler
app.post('/webhooks/payments', async (req, res) => {
const signature = req.headers['x-signature'];
// Verify signature
if (!verifyWebhookSignature(JSON.stringify(req.body), signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = req.body;
// Handle the event
switch (event.type) {
case 'payment.succeeded':
const payment = event.data.object;
console.log('Payment confirmed:', payment.tx_hash);
await fulfillOrder(payment.metadata.order_id);
break;
case 'payment.authorized':
const authorizedPayment = event.data.object;
console.log('Payment authorized:', authorizedPayment.id);
await reserveInventory(authorizedPayment.metadata.order_id);
break;
case 'payment.captured':
const capturedPayment = event.data.object;
console.log('Payment captured:', capturedPayment.id);
await fulfillOrder(capturedPayment.metadata.order_id);
break;
case 'payment.refunded':
const refundedPayment = event.data.object;
console.log('Payment refunded:', refundedPayment.id);
await updateOrderStatus(refundedPayment.metadata.order_id, 'refunded');
break;
case 'subscription.created':
const subscription = event.data.object;
console.log('Subscription created:', subscription.id);
await grantAccess(subscription.customer_id);
break;
case 'subscription.past_due':
const pastDueSub = event.data.object;
await notifyPaymentFailure(pastDueSub.customer_email);
break;
case 'subscription.cancelled':
const cancelledSub = event.data.object;
await revokeAccess(cancelledSub.customer_id);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).send('OK');
});Testing Webhooks
Test Mode Events
Events from test mode transactions are sent to your configured webhook endpoint just like live events. Use test mode to verify your integration.
Local Development
For local development, use a tool like ngrok to expose your local server:
ngrok http 3000Then configure the ngrok URL as your webhook endpoint in the dashboard.
Webhook Headers
Each webhook request includes the following headers:
| Header | Description |
|---|---|
| Content-Type | application/json |
| X-Signature | HMAC-SHA256 signature of the request body |
| X-Event-Id | Unique identifier for the event |
| X-Event-Type | The type of event (e.g., payment.succeeded) |
| X-Timestamp | Unix timestamp when the event was created |
Troubleshooting
Events not being received
- Verify your endpoint URL is correct and publicly accessible
- Check that your endpoint accepts POST requests
- Ensure your firewall allows requests from our IP addresses
Signature verification failing
- Confirm you’re using the correct webhook secret
- Ensure you’re verifying against the raw request body (before JSON parsing)
- Check that you’re using the correct hashing algorithm (SHA256)
Duplicate events
- Use the
event.idfield to deduplicate events - Implement idempotent event handlers
