Webhook Event Types
The payment gateway sends webhook events to notify your application about payment lifecycle changes.
Webhook Headers
All webhook requests include these headers:
X-PaymentService-Event: payment.completed
X-PaymentService-Timestamp: 1706356245
X-PaymentService-Signature: a1b2c3d4e5f6...
Content-Type: application/json
| Header | Description |
|---|---|
X-PaymentService-Event | The event type that triggered this webhook |
X-PaymentService-Timestamp | Unix timestamp when the webhook was sent |
X-PaymentService-Signature | HMAC-SHA256 signature for verification |
Payload Structure
All webhook payloads contain the full payment object:
{
"payment": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"userId": "...",
"status": "RECEIVED",
"reference": "ORDER-123",
"referenceLabel": "Order #123",
"currencyCode": "USD",
"settlementCurrencyCode": "USDT",
"amount": {
"value": 100.00,
"amount": 10000,
"formatted": "$100.00"
},
"receivedAmount": {
"value": 100.00,
"amount": 10000,
"formatted": "$100.00"
},
"settledAmount": {
"value": 100.00,
"amount": 10000,
"formatted": "$100.00"
},
"confirmedAt": "2026-01-15T10:30:00Z",
"foundAt": "2026-01-15T10:15:00Z",
"date": "2026-01-15T10:00:00Z"
}
}
Available Events
payment.completed
Triggered when the full payment amount is received and confirmed on‑chain. This is the terminal success state.
Status: RECEIVED
This is the primary event to listen for — it indicates the payment is complete and you should fulfill the order. Key your fulfillment off the event type (payment.completed), not the status string.
payment.received
Triggered when funds are received and confirmed on the blockchain.
Status: RECEIVED
This fires when the blockchain confirms the transaction. Use this if you want to act before the full confirmation cycle.
payment.underpaid
Triggered when the received amount is less than the requested amount.
Status: UNDERPAID
{
"payment": {
"id": "...",
"status": "UNDERPAID",
"amount": {
"value": 100.00,
"amount": 10000,
"formatted": "$100.00"
},
"receivedAmount": {
"value": 95.00,
"amount": 9500,
"formatted": "$95.00"
},
"remainingAmount": {
"value": 5.00,
"amount": 500,
"formatted": "$5.00"
}
}
}
payment.refund
Triggered when a refund is completed for a payment.
Status: REFUND
{
"payment": {
"id": "...",
"status": "REFUND",
"refund": true,
"refundedAmount": {
"value": 100.00,
"amount": 10000,
"formatted": "$100.00"
}
}
}
Event Summary
| Event | Description | Payment Status |
|---|---|---|
payment.completed | Payment fully received | RECEIVED |
payment.received | Funds received on blockchain | RECEIVED |
payment.underpaid | Insufficient amount received | UNDERPAID |
payment.refund | Refund completed | REFUND |
Handling Events
JavaScript Example
app.post('/webhooks/payments', (req, res) => {
const eventType = req.headers['x-paymentservice-event'];
const payment = req.body.payment;
switch (eventType) {
case 'payment.completed':
// Payment confirmed — fulfill the order
await updateOrderStatus(payment.reference, 'paid');
break;
case 'payment.received':
// Funds received — optional early notification
await notifyPaymentReceived(payment.reference);
break;
case 'payment.underpaid':
// Underpayment — notify customer
await handleUnderpayment(payment);
break;
case 'payment.refund':
// Refund completed — update records
await handleRefund(payment);
break;
}
res.status(200).send('OK');
});
Best Practices
- Always verify the webhook signature before processing events
- Process events idempotently — the same event may be delivered multiple times
- Respond with 200 quickly — process async if needed to avoid timeouts
- Log all received events for debugging and audit trails
- Handle unknown event types gracefully — don't crash on unrecognized events