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

# Webhooks

> Receive real-time event notifications when transactions, transfers, wallet operations, and other events occur in your TagPay integration.

Webhooks let your server receive automatic HTTP POST notifications when events happen in your TagPay account — such as a wallet credit, a transfer completing, or a new customer being created. Instead of polling the API for state changes, TagPay pushes event data directly to your endpoint.

***

## Webhook modes

TagPay supports two delivery modes. Choose the one that fits your integration:

<CardGroup cols={2}>
  <Card title="Legacy mode" icon="link">
    All events are delivered to a single `callbackURL` configured on your merchant account. Simple to set up, but offers no per-event filtering or delivery tracking in the API.
  </Card>

  <Card title="Subscription mode" icon="bell">
    Subscribe to specific event types with dedicated endpoints. Supports delivery history, replays, and per-subscription testing. Recommended for production integrations.
  </Card>
</CardGroup>

You can also set the mode to `BOTH` to deliver events to your callback URL **and** all active subscriptions simultaneously.

### Set your webhook mode

Use `PUT /merchant/webhook/settings` to switch between modes:

```bash cURL theme={null}
curl -X PUT https://api.tagpay.ng/v1/merchant/webhook/settings \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookMode": "SUBSCRIPTIONS"
  }'
```

Valid values for `webhookMode` are `LEGACY`, `SUBSCRIPTIONS`, or `BOTH`.

***

## Creating a subscription

Use `POST /merchant/webhook/subscribe` to register a URL that receives events for specific event types.

```bash cURL theme={null}
curl -X POST https://api.tagpay.ng/v1/merchant/webhook/subscribe \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/tagpay",
    "events": ["transaction.success", "wallet.credit", "transfer.completed"],
    "description": "Production event handler"
  }'
```

```json Response theme={null}
{
  "success": true,
  "data": {
    "id": "sub_abc123",
    "url": "https://your-server.com/webhooks/tagpay",
    "events": ["transaction.success", "wallet.credit", "transfer.completed"],
    "description": "Production event handler",
    "active": true,
    "createdAt": "2026-04-08T10:00:00.000Z"
  }
}
```

| Field         | Type   | Required | Description                                                     |
| ------------- | ------ | -------- | --------------------------------------------------------------- |
| `url`         | string | Yes      | The HTTPS endpoint that receives event POSTs                    |
| `events`      | array  | Yes      | List of event types to subscribe to. Use `["*"]` for all events |
| `description` | string | No       | A label to identify this subscription                           |

<Warning>
  Webhook URLs must use HTTPS in production. HTTP endpoints are blocked.
</Warning>

### Managing subscriptions

<CodeGroup>
  ```bash List subscriptions theme={null}
  curl -X GET https://api.tagpay.ng/v1/merchant/webhook/subscriptions \
    -H "Authorization: Bearer <your-token>"
  ```

  ```bash Get a subscription theme={null}
  curl -X GET https://api.tagpay.ng/v1/merchant/webhook/subscription/sub_abc123 \
    -H "Authorization: Bearer <your-token>"
  ```

  ```bash Update a subscription theme={null}
  curl -X PUT https://api.tagpay.ng/v1/merchant/webhook/subscription/sub_abc123 \
    -H "Authorization: Bearer <your-token>" \
    -H "Content-Type: application/json" \
    -d '{
      "events": ["transaction.success", "transaction.failed", "wallet.credit"],
      "active": true
    }'
  ```

  ```bash Delete a subscription theme={null}
  curl -X DELETE https://api.tagpay.ng/v1/merchant/webhook/subscription/sub_abc123 \
    -H "Authorization: Bearer <your-token>"
  ```
</CodeGroup>

***

## Available event types

| Event                 | Description                                      |
| --------------------- | ------------------------------------------------ |
| `transaction.success` | A transaction completed successfully             |
| `transaction.failed`  | A transaction failed                             |
| `transaction.pending` | A transaction is pending processing              |
| `wallet.credit`       | A customer wallet was credited                   |
| `wallet.debit`        | A customer wallet was debited                    |
| `customer.created`    | A new customer wallet was created                |
| `card.linked`         | A card was linked to a customer wallet           |
| `transfer.completed`  | A bank or wallet transfer completed successfully |
| `transfer.failed`     | A bank or wallet transfer failed                 |
| `*`                   | Subscribe to all event types                     |

To retrieve the authoritative list of supported events from the API:

```bash cURL theme={null}
curl -X GET https://api.tagpay.ng/v1/merchant/webhook/events \
  -H "Authorization: Bearer <your-token>"
```

***

## Webhook payload structure

All webhook deliveries POST a JSON body in this format:

```json theme={null}
{
  "event": "transaction.success",
  "data": {
    "transactionId": "txn_001",
    "reference": "TXN-2026-001",
    "type": "credit",
    "amount": 50000,
    "fee": 0,
    "currency": "NGN",
    "status": "success",
    "description": "Wallet credit",
    "timestamp": "2026-04-08T10:00:00.000Z",
    "metadata": {
      "merchantId": "mer_xyz",
      "merchantName": "Acme Payments"
    }
  }
}
```

For wallet events, the `data` object includes balance information:

```json theme={null}
{
  "event": "wallet.credit",
  "data": {
    "walletId": "wal_001",
    "accountNumber": "1234567890",
    "previousBalance": 10000,
    "newBalance": 60000,
    "amount": 50000,
    "type": "credit",
    "reference": "REF-2026-001",
    "timestamp": "2026-04-08T10:00:00.000Z"
  }
}
```

***

## Verifying webhook signatures

TagPay signs every webhook delivery so you can confirm it originated from TagPay and has not been tampered with. Two signature headers are sent with each request:

| Header                | Algorithm   | Description                                             |
| --------------------- | ----------- | ------------------------------------------------------- |
| `x-glide-signature`   | HMAC-SHA512 | Legacy signature — hash of the raw JSON payload         |
| `x-webhook-signature` | HMAC-SHA256 | Timestamp-prefixed signature for replay protection      |
| `x-webhook-timestamp` | —           | Unix timestamp in seconds used in the SHA-256 signature |

Your merchant private key (available from `GET /merchant/my-access-keys`) is used as the signing secret.

### Verify with Node.js

```javascript theme={null}
const crypto = require("crypto");

function verifyWebhookSignature(req, merchantPrivateKey) {
  const payload = JSON.stringify(req.body);

  // Verify the SHA-512 legacy signature
  const expectedLegacySignature = crypto
    .createHmac("sha512", merchantPrivateKey)
    .update(payload)
    .digest("hex");

  const legacySignature = req.headers["x-glide-signature"];
  const legacyValid = crypto.timingSafeEqual(
    Buffer.from(expectedLegacySignature, "hex"),
    Buffer.from(legacySignature, "hex")
  );

  // Verify the SHA-256 timestamp signature
  const timestamp = req.headers["x-webhook-timestamp"];
  const timestampedPayload = `${timestamp}.${payload}`;

  const expectedTimestampSignature = crypto
    .createHmac("sha256", merchantPrivateKey)
    .update(timestampedPayload)
    .digest("hex");

  const timestampSignature = req.headers["x-webhook-signature"];
  const timestampValid = crypto.timingSafeEqual(
    Buffer.from(expectedTimestampSignature, "hex"),
    Buffer.from(timestampSignature, "hex")
  );

  // Reject stale payloads (older than 5 minutes)
  const now = Math.floor(Date.now() / 1000);
  const isRecent = Math.abs(now - parseInt(timestamp, 10)) < 300;

  return (legacyValid || timestampValid) && isRecent;
}

// Express route example
app.post("/webhooks/tagpay", express.json(), (req, res) => {
  const isValid = verifyWebhookSignature(req, process.env.TAGPAY_PRIVATE_KEY);

  if (!isValid) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const { event, data } = req.body;

  switch (event) {
    case "transaction.success":
      // Handle successful transaction
      break;
    case "wallet.credit":
      // Handle wallet credit
      break;
    case "transfer.completed":
      // Handle completed transfer
      break;
    default:
      console.log("Unhandled event:", event);
  }

  // Always return 2xx to acknowledge receipt
  res.status(200).json({ received: true });
});
```

<Tip>
  Always return a `200` status code promptly to acknowledge receipt. TagPay considers any non-2xx response a delivery failure and will retry.
</Tip>

***

## Delivery history

View the delivery status of all webhook events for your account:

```bash cURL theme={null}
curl -X GET "https://api.tagpay.ng/v1/merchant/webhook/deliveries?status=failed&page=1" \
  -H "Authorization: Bearer <your-token>"
```

| Query param      | Description                                             |
| ---------------- | ------------------------------------------------------- |
| `status`         | Filter by `pending`, `success`, `failed`, or `retrying` |
| `eventType`      | Filter by event type (e.g., `transaction.success`)      |
| `subscriptionId` | Filter by a specific subscription                       |
| `startDate`      | ISO date string for the start of the range              |
| `endDate`        | ISO date string for the end of the range                |
| `page`           | Page number (default: 1)                                |

To inspect a specific delivery:

```bash cURL theme={null}
curl -X GET https://api.tagpay.ng/v1/merchant/webhook/delivery/del_001 \
  -H "Authorization: Bearer <your-token>"
```

***

## Replaying failed deliveries

If a delivery failed — for example, because your server was temporarily down — you can replay it:

```bash cURL theme={null}
curl -X POST https://api.tagpay.ng/v1/merchant/webhook/replay/del_001 \
  -H "Authorization: Bearer <your-token>"
```

This creates a new delivery record with a fresh payload signing and sends the original event data to the subscription's URL.

***

## Testing a webhook endpoint

Before going live, verify your endpoint handles events correctly using `POST /merchant/webhook/test/:id`, where `:id` is the subscription ID.

```bash cURL theme={null}
curl -X POST https://api.tagpay.ng/v1/merchant/webhook/test/sub_abc123 \
  -H "Authorization: Bearer <your-token>"
```

TagPay sends a `test.webhook` event to your endpoint and returns the HTTP status code and response time. The test payload looks like:

```json theme={null}
{
  "event": "test.webhook",
  "data": {
    "message": "This is a test webhook from TagPay",
    "timestamp": "2026-04-08T10:00:00.000Z",
    "subscriptionId": "sub_abc123",
    "merchantId": "mer_xyz"
  }
}
```

***

## Retry behavior

When a delivery fails (non-2xx response or connection timeout), TagPay retries automatically with exponential backoff:

| Attempt     | Delay     |
| ----------- | --------- |
| 1 (initial) | Immediate |
| 2           | 1 minute  |
| 3           | 5 minutes |

After 3 failed attempts, the delivery is marked as `failed` and no further retries are made. Use the replay endpoint to manually retry failed deliveries.

<Note>
  TagPay enforces a rate limit of 100 webhook deliveries per minute per subscription. Deliveries that exceed this limit are not sent and are recorded as rate-limited failures.
</Note>
