Webhooks Guide
Webhooks allow you to receive real-time notifications when events happen in your SwiftPay account.
How Webhooks Work
Event Occurs
An event happens (e.g., payment succeeds, refund created)
Webhook Sent
SwiftPay sends an HTTP POST request to your endpoint
You Process
Your server receives and processes the event
Acknowledge
Return a 2xx status to acknowledge receipt
Setting Up Webhooks
1. Create a Webhook Endpoint
First, create an endpoint in your application to receive webhooks:
const express = require ( 'express' );
const crypto = require ( 'crypto' );
const app = express ();
// Use raw body for signature verification
app . post ( '/webhooks/swiftpay' ,
express . raw ({ type: 'application/json' }),
( req , res ) => {
const signature = req . headers [ 'x-webhook-signature' ];
const timestamp = req . headers [ 'x-webhook-timestamp' ];
// Verify signature
if ( ! verifyWebhookSignature ( req . body , signature , timestamp )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
const event = JSON . parse ( req . body );
// Handle the event
switch ( event . type ) {
case 'checkout.succeeded' :
handlePaymentSuccess ( event . data );
break ;
case 'checkout.failed' :
handlePaymentFailure ( event . data );
break ;
case 'withdrawal.paid' :
handleWithdrawalPaid ( event . data );
break ;
case 'withdrawal.failed' :
handleWithdrawalFailed ( event . data );
break ;
}
res . status ( 200 ). send ( 'OK' );
}
);
2. Register the Endpoint
Register your webhook endpoint via the API:
curl -X POST https://api.swiftpay.cx/api/webhooks \
-H "Authorization: Bearer mp_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yoursite.com/webhooks/swiftpay",
"events": ["checkout.succeeded", "checkout.failed", "refund.created"]
}'
Response:
{
"success" : true ,
"data" : {
"endpoint" : {
"id" : "wh_abc123" ,
"url" : "https://yoursite.com/webhooks/swiftpay" ,
"events" : [ "checkout.succeeded" , "checkout.failed" , "withdrawal.paid" ],
"enabled" : true
},
"secret" : "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
Save the secret securely! It’s only shown once and is required to verify
webhook signatures.
Verifying Signatures
SwiftPay signs webhook events using a hash-based message authentication code (HMAC) with SHA-256. The signature is sent in the X-Webhook-Signature header.
The signature header contains a timestamp and one or more signatures. The timestamp is prefixed by t=, and each signature is prefixed by a scheme. Schemes start with v, followed by an integer. Currently, the only valid signature scheme is v1.
Example Header:
X-Webhook-Signature: t=1767890590,v1=ed9254fedf402abde774b82de99daeb08c6e5b9add8c5d944040107c74169d33
Format Breakdown:
t=1767890590 - Unix timestamp when the webhook was sent
v1=ed9254... - HMAC-SHA256 signature of the payload
How Signing Works
Signed Payload : SwiftPay constructs a string by concatenating:
The timestamp (t value)
The character .
The actual JSON payload (stringified request body)
Example: 1767890590.{"id":"evt_123","type":"checkout.succeeded",...}
Signature Generation : The signed payload is hashed using HMAC-SHA256 with your webhook secret as the key
Header Construction : The timestamp and signature are combined into the format shown above
const crypto = require ( 'crypto' );
function verifyWebhookSignature ( payload , signatureHeader , secret ) {
// 1. Extract timestamp and signature from header
const parts = signatureHeader . split ( ',' );
const timestamp = parts . find ( p => p . startsWith ( 't=' )). split ( '=' )[ 1 ];
const signature = parts . find ( p => p . startsWith ( 'v1=' )). split ( '=' )[ 1 ];
// 2. Check timestamp is recent (within 5 minutes) to prevent replay attacks
const currentTime = Math . floor ( Date . now () / 1000 );
if ( Math . abs ( currentTime - parseInt ( timestamp )) > 300 ) {
return false ;
}
// 3. Create the signed payload
const signedPayload = ` ${ timestamp } . ${ payload } ` ;
// 4. Calculate expected signature
const expectedSignature = crypto
. createHmac ( 'sha256' , secret )
. update ( signedPayload )
. digest ( 'hex' );
// 5. Compare signatures using constant-time comparison
return crypto . timingSafeEqual (
Buffer . from ( signature , 'hex' ),
Buffer . from ( expectedSignature , 'hex' )
);
}
Event Types
Checkout Events
Event Description checkout.succeededPayment completed successfully checkout.failedPayment attempt failed
Withdrawal Events
Event Description withdrawal.paidWithdrawal was paid withdrawal.failedWithdrawal failed
Event Payload
All webhook events follow this structure:
{
"id" : "evt_abc123" ,
"eventType" : "checkout.succeeded" ,
"eventId" : "evt_checkout_xyz789" ,
"createdAt" : "2024-01-15T10:30:00Z" ,
"payload" : {
"checkout_id" : "sess_xyz789" ,
"amount" : 2999 ,
"currency" : "USD" ,
"customer_email" : "[email protected] " ,
"payment_method" : "card" ,
"completed_at" : "2024-01-15T10:30:00Z"
}
}
checkout.succeeded Payload
{
"sessionId" : "sess_xyz789" ,
"amount" : 2999 ,
"amountInDecimals" : "29.99" ,
"currency" : "USD" ,
"customerEmail" : "[email protected] " ,
"customerName" : "John Doe" ,
"timestamp" : "2024-01-15T10:30:00Z" ,
"shipping" : {
"addressLine1" : "123 Main St" ,
"city" : "San Francisco" ,
"state" : "CA" ,
"postalCode" : "94102" ,
"country" : "US"
}
}
refund.created Payload
{
"refundId" : "ref_abc123" ,
"sessionId" : "sess_xyz789" ,
"amount" : 2999 ,
"reason" : "Customer requested" ,
"status" : "pending" ,
"timestamp" : "2024-01-15T11:00:00Z"
}
Retry Logic
If your endpoint doesn’t return a 2xx status, SwiftPay will retry:
Attempt Delay 1 Immediate 2 1 minute 3 5 minutes 4 30 minutes 5 2 hours 6 8 hours 7 24 hours
After 7 failed attempts, the delivery is marked as failed.
Best Practices
Return a 200 response immediately, then process the event asynchronously. This prevents timeouts. app . post ( '/webhooks' , async ( req , res ) => {
// Acknowledge receipt immediately
res . status ( 200 ). send ( 'OK' );
// Process async
processWebhook ( req . body ). catch ( console . error );
});
Webhooks may be sent multiple times. Use the eventId to ensure idempotent processing: async function processWebhook ( event ) {
// Check if already processed
const exists = await db . webhookEvents . findOne ({ eventId: event . eventId });
if ( exists ) return ;
// Process and mark as handled
await handleEvent ( event );
await db . webhookEvents . create ({ eventId: event . eventId });
}
Always use HTTPS endpoints. HTTP endpoints are rejected.
Monitor webhook delivery status in your dashboard and set up alerts for failures.
Testing Webhooks
Local Development
Use a tool like ngrok to expose your local server:
Then register the ngrok URL as your webhook endpoint.
Managing Endpoints
List Endpoints
curl https://api.swiftpay.cx/api/webhooks \
-H "Authorization: Bearer mp_live_your_api_key"
Update Endpoint
curl -X PATCH https://api.swiftpay.cx/api/webhooks/{id} \
-H "Authorization: Bearer mp_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"events": ["checkout.succeeded", "refund.created"],
"isActive": true
}'
Delete Endpoint
curl -X DELETE https://api.swiftpay.cx/api/webhooks/{id} \
-H "Authorization: Bearer mp_live_your_api_key"