Skip to main content

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:

  1. Go to DashboardSettingsWebhooks
  2. Click Add Endpoint
  3. Enter your URL and select events
  4. Save and copy the Signing Secret

Webhook Events

EventDescription
payment.completedPayment successful
payment.failedPayment failed
payment.expiredPayment timed out
payment.refundedRefund completed
settlement.completedFunds settled to wallet
settlement.failedSettlement 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:

AttemptDelay
1Immediate
25 minutes
330 minutes
42 hours
56 hours
6-1012 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:

  1. Go to WebhooksYour Endpoint
  2. Click Send Test Event
  3. Select event type
  4. Click Send

Replay Webhooks

Replay failed webhooks from the dashboard:

  1. Go to WebhooksRecent Deliveries
  2. Find the failed delivery
  3. Click Replay

Next Steps