> ## Documentation Index
> Fetch the complete documentation index at: https://docs.swiftpay.cx/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks Guide

> Set up and handle webhook events

# Webhooks Guide

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

## How Webhooks Work

<Steps>
  <Step title="Event Occurs">
    An event happens (e.g., payment succeeds, refund created)
  </Step>

  <Step title="Webhook Sent">
    SwiftPay sends an HTTP POST request to your endpoint
  </Step>

  <Step title="You Process">Your server receives and processes the event</Step>
  <Step title="Acknowledge">Return a 2xx status to acknowledge receipt</Step>
</Steps>

## Setting Up Webhooks

### 1. Create a Webhook Endpoint

First, create an endpoint in your application to receive webhooks:

<CodeGroup>
  ```javascript Express.js theme={null}
  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');

  }
  );

  ```

  ```python Flask theme={null}
  from flask import Flask, request
  import hmac
  import hashlib

  app = Flask(__name__)

  @app.route('/webhooks/swiftpay', methods=['POST'])
  def webhook():
      signature = request.headers.get('X-Webhook-Signature')
      timestamp = request.headers.get('X-Webhook-Timestamp')

      # Verify signature
      if not verify_signature(request.data, signature, timestamp):
          return 'Invalid signature', 401

      event = request.json

      # Handle the event
      if event['type'] == 'checkout.succeeded':
          handle_payment_success(event['data'])
      elif event['type'] == 'checkout.failed':
          handle_payment_failure(event['data'])
      elif event['type'] == 'withdrawal.paid':
          handle_withdrawal_paid(event['data'])
      elif event['type'] == 'withdrawal.failed':
          handle_withdrawal_failed(event['data'])

      return 'OK', 200
  ```

  ```php PHP theme={null}
  <?php
  $payload = file_get_contents('php://input');
  $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];
  $timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];

  // Verify signature
  if (!verifySignature($payload, $signature, $webhookSecret)) {
      http_response_code(401);
      exit('Invalid signature');
  }

  $event = json_decode($payload, true);

  switch ($event['type']) {
      case 'checkout.succeeded':
          handlePaymentSuccess($event['data']);
          break;
      case 'checkout.failed':
          handlePaymentFailure($event['data']);
          break;
      case 'refund.completed':
          handleRefundCompleted($event['data']);
          break;
      case 'refund.failed':
          handleRefundFailed($event['data']);
          break;
  }

  http_response_code(200);
  echo 'OK';
  ```
</CodeGroup>

### 2. Register the Endpoint

Register your webhook endpoint via the API:

```bash theme={null}
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.completed", "refund.failed"]
  }'
```

Response:

```json theme={null}
{
   "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"
   }
}
```

<Warning>
  Save the `secret` securely! It's only shown once and is required to verify
  webhook signatures.
</Warning>

## 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

<CodeGroup>
  ```javascript JavaScript theme={null}
  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')
  );
  }

  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time

  def verify_signature(payload, signature_header, secret):
      # 1. Extract timestamp and signature from header
      parts = signature_header.split(',')
      timestamp = next(p.split('=')[1] for p in parts if p.startswith('t='))
      signature = next(p.split('=')[1] for p in parts if p.startswith('v1='))

      # 2. Check timestamp is recent (within 5 minutes)
      current_time = int(time.time())
      if abs(current_time - int(timestamp)) > 300:
          return False

      # 3. Create the signed payload
      signed_payload = f"{timestamp}.{payload.decode()}"

      # 4. Calculate expected signature
      expected_signature = hmac.new(
          secret.encode(),
          signed_payload.encode(),
          hashlib.sha256
      ).hexdigest()

      # 5. Compare signatures
      return hmac.compare_digest(signature, expected_signature)
  ```

  ```php PHP theme={null}
  function verifySignature($payload, $signatureHeader, $secret) {
      // 1. Extract timestamp and signature from header
      $parts = explode(',', $signatureHeader);
      $timestamp = "";
      $signature = "";

      foreach ($parts as $part) {
          if (str_starts_with($part, 't=')) {
              $timestamp = substr($part, 2);
          } elseif (str_starts_with($part, 'v1=')) {
              $signature = substr($part, 3);
          }
      }

      // 2. Check timestamp is recent (within 5 minutes)
      $currentTime = time();
      if (abs($currentTime - intval($timestamp)) > 300) {
          return false;
      }

      // 3. Create the signed payload
      $signedPayload = $timestamp . '.' . $payload;

      // 4. Calculate expected signature
      $expectedSignature = hash_hmac('sha256', $signedPayload, $secret);

      // 5. Compare signatures
      return hash_equals($expectedSignature, $signature);
  }
  ```
</CodeGroup>

## Event Types

### Checkout Events

| Event                | Description                    |
| -------------------- | ------------------------------ |
| `checkout.succeeded` | Payment completed successfully |
| `checkout.failed`    | Payment attempt failed         |

### Withdrawal Events

| Event               | Description         |
| ------------------- | ------------------- |
| `withdrawal.paid`   | Withdrawal was paid |
| `withdrawal.failed` | Withdrawal failed   |

## Event Payload

All webhook events follow this structure:

```json theme={null}
{
   "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": "customer@example.com",
      "payment_method": "card",
      "completed_at": "2024-01-15T10:30:00Z"
   }
}
```

### checkout.succeeded Payload

```json theme={null}
{
   "sessionId": "sess_xyz789",
   "amount": 2999,
   "amountInDecimals": "29.99",
   "currency": "USD",
   "customerEmail": "customer@example.com",
   "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

```json theme={null}
{
   "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

<AccordionGroup>
  <Accordion title="Return 200 Quickly" icon="bolt">
    Return a 200 response immediately, then process the event asynchronously. This prevents timeouts.

    ```javascript theme={null}
    app.post('/webhooks', async (req, res) => {
      // Acknowledge receipt immediately
      res.status(200).send('OK');

      // Process async
      processWebhook(req.body).catch(console.error);
    });
    ```
  </Accordion>

  <Accordion title="Handle Idempotency" icon="repeat">
    Webhooks may be sent multiple times. Use the `eventId` to ensure idempotent processing:

    ```javascript theme={null}
    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 });
    }
    ```
  </Accordion>

  <Accordion title="Use HTTPS" icon="lock">
    Always use HTTPS endpoints. HTTP endpoints are rejected.
  </Accordion>

  <Accordion title="Monitor Failures" icon="chart-line">
    Monitor webhook delivery status in your dashboard and set up alerts for failures.
  </Accordion>
</AccordionGroup>

## Testing Webhooks

### Local Development

Use a tool like [ngrok](https://ngrok.com) to expose your local server:

```bash theme={null}
ngrok http 3000
```

Then register the ngrok URL as your webhook endpoint.

## Managing Endpoints

### List Endpoints

```bash theme={null}
curl https://api.swiftpay.cx/api/webhooks \
  -H "Authorization: Bearer mp_live_your_api_key"
```

### Update Endpoint

```bash theme={null}
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

```bash theme={null}
curl -X DELETE https://api.swiftpay.cx/api/webhooks/{id} \
  -H "Authorization: Bearer mp_live_your_api_key"
```

```
```
