Skip to main content

Webhooks Guide

Webhooks allow you to receive real-time notifications when events happen in your SwiftPay account.

How Webhooks Work

1

Event Occurs

An event happens (e.g., payment succeeds, refund created)
2

Webhook Sent

SwiftPay sends an HTTP POST request to your endpoint
3

You Process

Your server receives and processes the event
4

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.

Signature Format

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

  1. 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",...}
  2. Signature Generation: The signed payload is hashed using HMAC-SHA256 with your webhook secret as the key
  3. 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

EventDescription
checkout.succeededPayment completed successfully
checkout.failedPayment attempt failed

Withdrawal Events

EventDescription
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:
AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 hours
724 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:
ngrok http 3000
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"