Webhooks
Receive real-time notifications for checkout events
Webhooks
Webhooks allow you to receive real-time notifications when events occur in your Send Pay Links account.
Events
| Event | Description |
|---|---|
checkout.created | A new checkout session was created |
checkout.completed | Checkout was successfully completed |
checkout.abandoned | Checkout was abandoned (expired without completing) |
payment.succeeded | Payment was successfully processed |
payment.failed | Payment processing failed |
order.confirmed | Order was confirmed by payment provider |
upsell.accepted | Customer accepted an upsell offer |
upsell.declined | Customer declined an upsell offer |
Webhook Payload
All webhook payloads follow this structure:
{
"id": "evt_abc123",
"type": "payment.succeeded",
"timestamp": "2024-01-01T12:00:00Z",
"data": {
"checkoutId": "chk_xyz789",
"orderId": "order_123",
"amount": 9999,
"currency": "USD",
"customer": {
"email": "customer@example.com",
"firstName": "John",
"lastName": "Doe"
},
"metadata": {
"plan": "premium"
}
}
}Configuring Webhooks
In the Dashboard
- Go to Settings > Webhooks
- Click Add Endpoint
- Enter your webhook URL
- Select the events you want to receive
- Click Save
Per-Checkout Webhooks
You can also specify a webhook URL for individual checkouts:
const token = await createCheckoutToken({
orderId: 'order_123',
amount: 9999,
webhookUrl: 'https://your-server.com/webhooks/checkout',
});Verifying Webhooks
All webhooks are signed using HMAC-SHA256. Verify the signature to ensure the webhook is authentic:
import crypto from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// In your webhook handler
app.post('/webhooks/checkout', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const event = req.body;
console.log('Received event:', event.type);
res.status(200).send('OK');
});Webhook Headers
Each webhook request includes these headers:
| Header | Description |
|---|---|
X-Webhook-ID | Unique ID for this webhook delivery |
X-Webhook-Signature | HMAC-SHA256 signature |
X-Webhook-Timestamp | Unix timestamp when webhook was sent |
Content-Type | Always application/json |
Retry Policy
If your endpoint returns a non-2xx response, we'll retry the webhook:
- Retry 1: After 5 minutes
- Retry 2: After 30 minutes
- Retry 3: After 2 hours
- Retry 4: After 8 hours
- Retry 5: After 24 hours
After 5 failed attempts, the webhook is marked as failed and won't be retried.
Best Practices
Respond quickly: Return a 2xx response within 30 seconds. Process the webhook asynchronously if needed.
- Verify signatures - Always verify the webhook signature before processing
- Handle duplicates - Use the event ID to detect and handle duplicate deliveries
- Respond quickly - Return a 200 immediately, then process async
- Log everything - Log webhook payloads for debugging
- Use HTTPS - Always use HTTPS endpoints
Example: Complete Handler
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const processedEvents = new Set();
app.post('/webhooks/checkout', async (req, res) => {
// 1. Verify signature
const signature = req.headers['x-webhook-signature'] as string;
const payload = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(payload)
.digest('hex');
if (signature !== expectedSignature) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// 2. Check for duplicates
const event = req.body;
if (processedEvents.has(event.id)) {
return res.status(200).send('Already processed');
}
processedEvents.add(event.id);
// 3. Respond immediately
res.status(200).send('OK');
// 4. Process async
try {
switch (event.type) {
case 'payment.succeeded':
await handlePaymentSuccess(event.data);
break;
case 'checkout.abandoned':
await sendAbandonedCartEmail(event.data);
break;
default:
console.log('Unhandled event type:', event.type);
}
} catch (error) {
console.error('Error processing webhook:', error);
}
});
async function handlePaymentSuccess(data: any) {
// Fulfill the order, send confirmation email, etc.
console.log('Order completed:', data.orderId);
}
async function sendAbandonedCartEmail(data: any) {
// Send recovery email
console.log('Abandoned cart:', data.checkoutId);
}Testing Webhooks
Use the webhook tester in the dashboard to send test events:
- Go to Settings > Webhooks
- Click on your endpoint
- Click Send Test Event
- Select an event type
- Click Send
For local development, use a tunnel service like ngrok:
ngrok http 3000
# Use the ngrok URL as your webhook endpoint