Webhooks
Receive real-time notifications for payment events.
Overview
Webhooks allow your application to receive automatic updates when payment events occur, eliminating the need for polling.
Configure Webhooks
Per-Payment Webhook
Set webhook URL when creating a payment:
const payment = await pelago.payments.create({
amount: 100,
currency: 'USD',
webhookUrl: 'https://mystore.com/api/webhooks/pelago',
// ...
});
Global Webhook (Dashboard)
Configure a default webhook for all payments:
- Go to Dashboard → Settings → Webhooks
- Click Add Endpoint
- Enter your URL and select events
- Save and copy the Signing Secret
Webhook Events
| Event | Description |
|---|---|
payment.completed | Payment successful |
payment.failed | Payment failed |
payment.expired | Payment timed out |
payment.refunded | Refund completed |
settlement.completed | Funds settled to wallet |
settlement.failed | Settlement failed |
Webhook Payload
Structure
{
"id": "evt_abc123",
"object": "event",
"type": "payment.completed",
"created": "2025-02-08T22:35:00Z",
"data": {
"paymentId": "pay_7xKp9mNq2vT",
"status": "completed",
"amount": 100.00,
"currency": "USD",
"cryptoAmount": "100.000000",
"cryptocurrency": "USDC",
"network": "stellar",
"merchantWallet": "GXXXXX...",
"customerWallet": "GCUST...",
"transactionHash": "abc123...def456",
"metadata": {
"orderId": "ORD-12345",
"customerId": "cust_abc123"
}
}
}
Event Types
payment.completed
{
"type": "payment.completed",
"data": {
"paymentId": "pay_xxx",
"transactionHash": "abc...",
"customerWallet": "GCUST..."
}
}
payment.failed
{
"type": "payment.failed",
"data": {
"paymentId": "pay_xxx",
"failureReason": "insufficient_funds",
"failureMessage": "Customer wallet has insufficient USDC"
}
}
payment.expired
{
"type": "payment.expired",
"data": {
"paymentId": "pay_xxx",
"expiredAt": "2025-02-08T23:00:00Z"
}
}
Signature Verification
Always verify webhook signatures to ensure authenticity.
JavaScript
import crypto from 'crypto';
import express from 'express';
const app = express();
// Use raw body for signature verification
app.post('/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-pelago-signature'];
const timestamp = req.headers['x-pelago-timestamp'];
const body = req.body.toString();
// Verify signature
const payload = timestamp + '.' + body;
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Verify timestamp (prevent replay attacks)
const eventTime = parseInt(timestamp);
const now = Date.now();
if (Math.abs(now - eventTime) > 300000) { // 5 minutes
return res.status(401).json({ error: 'Stale timestamp' });
}
// Process event
const event = JSON.parse(body);
handleEvent(event);
res.status(200).json({ received: true });
}
);
async function handleEvent(event) {
switch (event.type) {
case 'payment.completed':
await fulfillOrder(event.data.metadata.orderId);
break;
case 'payment.failed':
await notifyCustomer(event.data.metadata.customerId);
break;
}
}
Python
import hmac
import hashlib
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Pelago-Signature')
timestamp = request.headers.get('X-Pelago-Timestamp')
body = request.get_data(as_text=True)
# Verify signature
payload = timestamp + '.' + body
expected = hmac.new(
os.environ['WEBHOOK_SECRET'].encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return jsonify({'error': 'Invalid signature'}), 401
# Verify timestamp
event_time = int(timestamp)
now = int(time.time() * 1000)
if abs(now - event_time) > 300000:
return jsonify({'error': 'Stale timestamp'}), 401
# Process event
event = request.get_json()
handle_event(event)
return jsonify({'received': True}), 200
Using the SDK
// Simplified verification with SDK
app.post('/webhook', express.json(), (req, res) => {
const signature = req.headers['x-pelago-signature'];
const timestamp = req.headers['x-pelago-timestamp'];
const isValid = pelago.webhooks.verify(
req.body,
signature,
timestamp,
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid');
}
// Process...
});
Retry Policy
Failed webhooks are retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 6 hours |
| 6-10 | 12 hours |
After 10 failures, the webhook is marked as failed. Check the dashboard for failed deliveries.
Best Practices
1. Respond Quickly
Return HTTP 200 immediately, then process asynchronously:
app.post('/webhook', async (req, res) => {
// Verify signature first
if (!verifySignature(req)) {
return res.status(401).send('Invalid');
}
// Acknowledge immediately
res.status(200).json({ received: true });
// Process asynchronously
processEventAsync(req.body);
});
async function processEventAsync(event: any) {
// Heavy processing here
await fulfillOrder(event.data.metadata.orderId);
await sendConfirmationEmail(event.data.metadata.email);
}
2. Handle Duplicates
Implement idempotency to handle retry scenarios:
const processedEvents = new Set(); // Use Redis in production
app.post('/webhook', (req, res) => {
const eventId = req.body.id;
if (processedEvents.has(eventId)) {
console.log('Duplicate event, skipping:', eventId);
return res.status(200).json({ received: true });
}
processedEvents.add(eventId);
handleEvent(req.body);
res.status(200).json({ received: true });
});
3. Log Everything
app.post('/webhook', (req, res) => {
console.log('Webhook received:', {
eventId: req.body.id,
type: req.body.type,
paymentId: req.body.data.paymentId,
timestamp: new Date().toISOString()
});
// ...
});
Testing Webhooks
Local Development
Use ngrok to expose your local server:
ngrok http 3000
# Output: https://abc123.ngrok.io
Set this URL as your webhook endpoint.
Webhook Tester
Send a test webhook from the dashboard:
- Go to Webhooks → Your Endpoint
- Click Send Test Event
- Select event type
- Click Send
Replay Webhooks
Replay failed webhooks from the dashboard:
- Go to Webhooks → Recent Deliveries
- Find the failed delivery
- Click Replay