Why Webhooks?
Get real-time notifications when payments complete, fail, or change status. Webhooks keep your system in sync without polling.
Instant Updates
Know immediately when payments complete or fail
Reliable Delivery
Automatic retries ensure you never miss events
Efficient
No need to constantly poll for status updates
Complete Information
Each webhook includes full payment details
Setup
Create an Endpoint
Create a route on your server to receive webhook POST requests.
Register Your URL
Contact our support team to register your webhook endpoint.
Verify Signatures
Always validate that webhooks come from Cheqpay (see security below).
Webhook Events
| Event | Triggered When | Recommended Action |
|---|
payment.completed | Payment successful | Fulfill order |
payment.failed | Payment declined | Notify customer, offer retry |
payment.refunded | Full refund processed | Cancel order, update records |
payment.partially_refunded | Partial refund issued | Update order amount |
spei.deposit_received | SPEI transfer received | Process and fulfill order |
Each webhook includes event details and payment information:
{
"eventId": "evt_abc123",
"eventType": "payment.completed",
"timestamp": "2025-10-30T14:30:00.000Z",
"data": {
"paymentOrderId": "ord_xyz789",
"paymentId": "pay_def456",
"externalId": "order-12345",
"status": "COMPLETED",
"amount": 10000,
"currency": "MXN",
"customerId": "cus_ghi012",
"customer": {
"firstName": "María",
"lastName": "González",
"email": "[email protected]"
},
"paymentMethod": {
"type": "card",
"card": {
"brand": "visa",
"last4": "1111"
}
}
}
}
Common Fields
| Field | Type | Description |
|---|
eventId | string | Unique event identifier |
eventType | string | Type of event |
timestamp | string | ISO 8601 timestamp |
data | object | Event-specific data |
data.paymentOrderId | string | Cheqpay payment order ID |
data.externalId | string | Your order ID |
data.status | string | Current payment status |
Handle Webhooks Properly
Requirements
Return 200 OK within 5 seconds. Process data asynchronously.
Always check authenticity before processing events.
Queue work for background processing. Don’t block the response.
Events may be sent multiple times. Make your handler idempotent.
Example Handler (Node.js)
const express = require('express');
const crypto = require('crypto');
app.post('/webhooks/cheqpay', express.json(), (req, res) => {
// 1. Verify signature first
const signature = req.headers['x-cheqpay-signature'];
if (!verifySignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
// 2. Respond immediately
res.status(200).send('OK');
// 3. Process in background
processWebhook(req.body);
});
function processWebhook(webhook) {
// Queue for async processing
queue.add('process-payment', {
eventId: webhook.eventId,
eventType: webhook.eventType,
data: webhook.data
});
}
Example Handler (Python)
from flask import Flask, request
import hmac
import hashlib
app = Flask(__name__)
@app.route('/webhooks/cheqpay', methods=['POST'])
def webhook():
# 1. Verify signature
signature = request.headers.get('X-Cheqpay-Signature')
if not verify_signature(request.data, signature):
return 'Invalid signature', 401
# 2. Respond immediately
# 3. Process async
webhook_data = request.json
queue.enqueue(process_webhook, webhook_data)
return 'OK', 200
def process_webhook(data):
event_type = data['eventType']
if event_type == 'payment.completed':
fulfill_order(data['data']['externalId'])
elif event_type == 'payment.failed':
notify_customer(data['data'])
# ... handle other events
Security
Verify every webhook to ensure it’s from Cheqpay.
Signature Verification
const crypto = require('crypto');
function verifySignature(payload, signature) {
const expected = crypto
.createHmac('sha256', YOUR_WEBHOOK_SECRET)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Python Example
import hmac
import hashlib
import json
def verify_signature(payload, signature):
expected = hmac.new(
YOUR_WEBHOOK_SECRET.encode(),
json.dumps(payload).encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
Your webhook secret is provided when you register your endpoint. Keep it secure and never commit it to version control.
Handle Duplicate Events
Webhooks may be delivered multiple times. Make your handler idempotent:
async function processPaymentCompleted(webhook) {
const eventId = webhook.eventId;
// Check if already processed
const existing = await db.webhookEvents.findOne({ eventId });
if (existing) {
console.log('Event already processed:', eventId);
return;
}
// Process the event
await fulfillOrder(webhook.data.externalId);
// Mark as processed
await db.webhookEvents.insert({
eventId,
processedAt: new Date()
});
}
Event-Specific Handling
Payment Completed
if (webhook.eventType === 'payment.completed') {
const { externalId, amount, customerId } = webhook.data;
// Fulfill the order
await orders.fulfill(externalId);
// Send confirmation email
await sendEmail(customerId, {
subject: 'Payment Received',
template: 'payment-confirmation'
});
// Update inventory
await inventory.reduce(externalId);
}
Payment Failed
if (webhook.eventType === 'payment.failed') {
const { externalId, customerId } = webhook.data;
// Notify customer
await sendEmail(customerId, {
subject: 'Payment Failed',
template: 'payment-failed',
data: { orderId: externalId }
});
// Update order status
await orders.update(externalId, { status: 'payment_failed' });
}
SPEI Deposit Received
if (webhook.eventType === 'spei.deposit_received') {
const { paymentOrderId, externalId, amount } = webhook.data;
// Verify amount matches
const order = await orders.findById(externalId);
if (order.amount === amount) {
// Fulfill order
await orders.fulfill(externalId);
// Send confirmation
await sendEmail(order.customerId, {
subject: 'SPEI Transfer Received',
template: 'spei-confirmed'
});
}
}
Automatic Retries
If your endpoint is unavailable, Cheqpay retries automatically:
Initial Attempt
Webhook is sent to your endpoint.
First Retry
If it fails, retry after 1 minute.
Second Retry
If still failing, retry after 5 minutes.
Final Retry
Last attempt after 30 minutes.
Notification
If all retries fail, you’ll be notified via email.
Set up monitoring to alert you of webhook failures before all retries are exhausted.
Testing Locally
Use ngrok to test webhooks during development:
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js # Running on port 3000
# Expose to internet
ngrok http 3000
# Output:
# Forwarding: https://abc123.ngrok.io -> http://localhost:3000
Register the ngrok URL with Cheqpay:
https://abc123.ngrok.io/webhooks/cheqpay
Now you can test webhooks with your local development server!
Remember to update to your production URL before going live.
Monitoring and Debugging
Log All Webhooks
app.post('/webhooks/cheqpay', (req, res) => {
// Log webhook for debugging
logger.info('Webhook received', {
eventId: req.body.eventId,
eventType: req.body.eventType,
timestamp: req.body.timestamp,
signature: req.headers['x-cheqpay-signature']
});
// ... rest of handler
});
Monitor Webhook Health
Track webhook delivery and processing:
const metrics = {
received: 0,
processed: 0,
failed: 0,
averageProcessingTime: 0
};
async function processWebhook(webhook) {
metrics.received++;
const start = Date.now();
try {
await handleWebhook(webhook);
metrics.processed++;
} catch (error) {
metrics.failed++;
logger.error('Webhook processing failed', { error, webhook });
}
const duration = Date.now() - start;
metrics.averageProcessingTime =
(metrics.averageProcessingTime + duration) / 2;
}
Best Practices
Always use HTTPS endpoints. Cheqpay will not send webhooks to HTTP URLs.
Never skip signature verification. This protects against spoofed webhooks.
Respond within 5 seconds. Use background jobs for actual processing.
Use eventId to track processed events and avoid duplicate processing.
Set up alerts for webhook failures. Investigate and fix issues quickly.
Log all webhook events for debugging and audit trails.
Webhook Endpoint Requirements
HTTPS Required
Must use secure HTTPS connection
Public Access
Endpoint must be publicly accessible
Fast Response
Return 200 OK within 5 seconds
Signature Verification
Verify signatures on all requests
Next Steps