> For the complete documentation index, see [llms.txt](https://docs.skypaybd.top/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skypaybd.top/integration-and-api/webhooks.md).

# Webhooks (IPN)

Webhooks allow merchant applications to react to payment state transitions in real time without relying on customer browser redirection or repetitive backend polling. Whenever a payment is completed or changes status, SkyPay dispatches an automated HTTP POST notification containing the transaction payload directly to your configured `webhook_url`.

***

## 1. Webhook Delivery Lifecycle

```
┌──────────────────────┐        ┌──────────────────────┐        ┌──────────────────────┐
│ Customer Payment     │───────>│ SkyPay Core Engine   │───────>│ Merchant Server      │
│ (MFS / QR / Crypto)  │        │ (Settlement Confirmed│        │ (Webhook Listener)   │
└──────────────────────┘        └──────────┬───────────┘        └──────────┬───────────┘
                                           │                               │
                                           │  HTTP POST (IPN Payload)      │
                                           ├──────────────────────────────>│
                                           │  X-SkyPay-Signature: HMAC     │
                                           │                               │
                                           │  HTTP 200 OK (within 5s)      │
                                           │<──────────────────────────────┤
                                           │                               │
                                           │ [If Timeout / Error: Retry]   │
                                           ├──────────────────────────────>│
```

### Trigger Conditions

* **Hosted Checkout (v1)**: Dispatched immediately when a customer completes payment on the hosted checkout page.
* **Headless API (v2)**: Dispatched upon transaction verification and database commit via `/api/v2/payment/verify`.

***

## 2. Incoming Payload Schema

The webhook payload is delivered as raw JSON in the request body.

### Payload Schema

| Property        | Type              | Nullable | Description                                                                               |
| --------------- | ----------------- | -------- | ----------------------------------------------------------------------------------------- |
| `paymentMethod` | String            | No       | Payment channel used: `bkash`, `nagad`, `rocket`, `upay`, `banglaqr`, `binance`, `bep20`. |
| `transactionId` | String            | No       | Unique verified transaction identifier (MFS TrxID, EVM TxHash, Binance Order ID).         |
| `paymentAmount` | String / Numeric  | No       | Formatted transaction settlement amount (e.g., `"1500.00"`).                              |
| `paymentFee`    | String / Numeric  | No       | Processing fee deducted by SkyPay (e.g., `"0.00"`).                                       |
| `status`        | String            | No       | Transaction state: `completed`, `pending`, `failed`.                                      |
| `meta_data`     | Object / Null     | Yes      | Custom metadata object provided during session initialization.                            |
| `timestamp`     | String (ISO 8601) | No       | UTC timestamp of event generation (e.g., `"2026-10-20T12:45:00Z"`).                       |

### Example Webhook JSON Body

```json
{
  "paymentMethod": "bkash",
  "transactionId": "BLM37A9B1X",
  "paymentAmount": "1500.00",
  "paymentFee": "0.00",
  "status": "completed",
  "meta_data": {
    "order_id": "ORD-99120",
    "user_id": 4821
  },
  "timestamp": "2026-10-20T12:45:00Z"
}
```

***

## 3. Webhook Signature Verification

To guarantee that the webhook notification originates from SkyPay and has not been intercepted, tampered with, or forged, SkyPay includes an HMAC-SHA256 signature in the HTTP request headers.

### Signature Headers

* **`X-SkyPay-Signature`**: Hex-encoded HMAC-SHA256 signature of the raw HTTP request body, calculated using your secret `BRAND-KEY`.

### Verification Algorithm

1. Read the **raw, unparsed** HTTP request body string.
2. Extract the signature string from the `X-SkyPay-Signature` header.
3. Compute the HMAC-SHA256 hash of the raw body using your merchant `BRAND-KEY` as the cryptographic secret.
4. Compare your calculated hash against the incoming signature using a **timing-safe string comparison function** to prevent timing attacks.

{% hint style="danger" %}
**Do Not Parse Before Verifying**: Always compute the HMAC on the exact byte sequence of the raw request payload before parsing JSON into an object or dictionary. Key reordering or whitespace normalization will cause signature verification to fail.
{% endhint %}

### Verification Implementations

#### Node.js / Express

```javascript
const express = require('express');
const crypto = require('crypto');

const app = express();

// Capture raw body buffer for HMAC verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/api/payment-webhook', (req, res) => {
  const signature = req.headers['x-skypay-signature'];
  const brandKey = process.env.SKYPAY_BRAND_KEY;

  if (!signature) {
    return res.status(401).json({ error: 'Missing X-SkyPay-Signature header' });
  }

  // Calculate HMAC-SHA256
  const expectedSignature = crypto
    .createHmac('sha256', brandKey)
    .update(req.rawBody)
    .digest('hex');

  // Timing-safe comparison
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expectedSignature, 'hex')
  );

  if (!isValid) {
    return res.status(403).json({ error: 'Invalid webhook signature' });
  }

  const event = req.body;
  console.log(`Payment confirmed: ${event.transactionId} for ${event.paymentAmount} BDT`);

  // Process order (ensure idempotency)
  // ...

  // Respond immediately with 200 OK
  return res.status(200).json({ status: true });
});
```

#### PHP (Native / Laravel)

```php
<?php

$signature = $_SERVER['HTTP_X_SKYPAY_SIGNATURE'] ?? '';
$brandKey = getenv('SKYPAY_BRAND_KEY') ?: 'your_active_brand_key';

// Read raw body
$rawPayload = file_get_contents('php://input');

if (empty($signature) || empty($rawPayload)) {
    http_response_code(400);
    exit(json_encode(['error' => 'Missing signature or payload']));
}

$expectedSignature = hash_hmac('sha256', $rawPayload, $brandKey);

if (!hash_equals($expectedSignature, $signature)) {
    http_response_code(403);
    exit(json_encode(['error' => 'Invalid cryptographic signature']));
}

$data = json_decode($rawPayload, true);

if ($data['status'] === 'completed') {
    $trxId = $data['transactionId'];
    $amount = $data['paymentAmount'];
    
    // Idempotent database check & credit
    // processOrder($trxId, $amount);
}

// Acknowledge receipt
http_response_code(200);
echo json_encode(['status' => true]);
```

#### Python (Flask)

```python
import os
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
BRAND_KEY = os.getenv("SKYPAY_BRAND_KEY", "your_active_brand_key").encode('utf-8')

@app.route('/api/payment-webhook', methods=['POST'])
def handle_webhook():
    incoming_sig = request.headers.get('X-SkyPay-Signature')
    if not incoming_sig:
        return jsonify({"error": "Missing signature header"}), 401

    raw_body = request.get_data()
    expected_sig = hmac.new(BRAND_KEY, raw_body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(incoming_sig, expected_sig):
        return jsonify({"error": "Invalid signature"}), 403

    payload = request.get_json()
    if payload.get("status") == "completed":
        trx_id = payload.get("transactionId")
        amount = payload.get("paymentAmount")
        # Idempotently credit user account
        # fulfill_order(trx_id, amount)

    return jsonify({"status": True}), 200
```

***

## 4. Retry Schedule & Server Acknowledgement

### Expected Acknowledgement

* **Status Code**: `HTTP 200 OK`
* **Response Body**: `{"status": true}` or plain text `OK`
* **Timeout Limit**: 5,000 milliseconds (5 seconds).

If your endpoint responds with any 4xx/5xx HTTP status code, experiences an SSL handshake failure, or fails to respond within 5 seconds, SkyPay categorizes the delivery as failed and queues an automated retry.

### Exponential Backoff Retry Matrix

SkyPay executes up to **8 delivery attempts** over a 24-hour window:

| Attempt #       | Delay From Prior Attempt | Elapsed Time Since Event |
| --------------- | ------------------------ | ------------------------ |
| **1 (Initial)** | Immediate                | 0 seconds                |
| **2**           | 5 minutes                | 5 minutes                |
| **3**           | 15 minutes               | 20 minutes               |
| **4**           | 30 minutes               | 50 minutes               |
| **5**           | 1 hour                   | 1 hour 50 minutes        |
| **6**           | 2 hours                  | 3 hours 50 minutes       |
| **7**           | 6 hours                  | 9 hours 50 minutes       |
| **8 (Final)**   | 12 hours                 | 21 hours 50 minutes      |

After 8 failed attempts, the webhook is marked as `DEAD_LETTER` and can only be redelivered manually via the Merchant Portal.

***

## 5. Idempotency & Concurrency Handling

Because network anomalies can cause webhooks to be re-transmitted, your listener must be strictly **idempotent** (processing the same notification multiple times must not result in duplicate order fulfillment or double balance credits).

### Recommended Implementation Strategy

1. **Unique Database Constraint**: Add a unique index to your orders/transactions table on the `transaction_id` column.
2. **Atomic Transaction Lock**: When receiving a webhook, acquire a row lock or Redis distributed lock on `transaction_id`:

   ```sql
   SELECT id, status FROM payments WHERE transaction_id = 'BLM37A9B1X' FOR UPDATE;
   ```
3. **State Check**: If the payment record is already marked as `COMPLETED`, ignore further processing and immediately return `HTTP 200 OK`.
4. **Fulfill & Commit**: If the record is still pending, mark it `COMPLETED`, credit the user account, and commit the database transaction.

***

## 6. Next Steps

To monitor the hardware relay devices powering MFS SMS verifications, review the Telemetry guide:

* [← Previous: Verify Payment](/integration-and-api/verify-payment.md)
* [Next: Device Health & Telemetry →](/system-and-devices/devices-telemetry.md)
