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

# Event Types

> Reference for all webhook event types, payload structure, and signature verification.

## List available event types

Retrieve the complete list of event types your subscriptions can listen to, along with human-readable descriptions.

```
GET https://api.tagpay.ng/v1/merchant/webhook/events
```

### Response

<ResponseField name="status" type="boolean">
  `true` when the request succeeded.
</ResponseField>

<ResponseField name="data" type="object[]">
  Array of event type descriptors.

  <Expandable title="Event type object">
    <ResponseField name="type" type="string">
      The event type string to use in your subscription's `events` array.
    </ResponseField>

    <ResponseField name="description" type="string">
      Human-readable description of when this event fires.
    </ResponseField>
  </Expandable>
</ResponseField>

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

<ResponseExample>
  ```json 200 theme={null}
  {
    "status": true,
    "message": "Available event types",
    "data": [
      { "type": "transaction.success", "description": "Triggered when a transaction completes successfully" },
      { "type": "transaction.failed", "description": "Triggered when a transaction fails" },
      { "type": "transaction.pending", "description": "Triggered when a transaction is pending" },
      { "type": "wallet.credit", "description": "Triggered when a wallet is credited" },
      { "type": "wallet.debit", "description": "Triggered when a wallet is debited" },
      { "type": "customer.created", "description": "Triggered when a new customer is created" },
      { "type": "card.linked", "description": "Triggered when a card is linked to a wallet" },
      { "type": "transfer.completed", "description": "Triggered when a transfer completes successfully" },
      { "type": "transfer.failed", "description": "Triggered when a transfer fails" },
      { "type": "*", "description": "Subscribe to ALL webhook events" }
    ]
  }
  ```
</ResponseExample>

***

## Event type reference

### Standardized events

These are the recommended event types for new integrations.

| Event                 | Description                                             |
| --------------------- | ------------------------------------------------------- |
| `transaction.success` | A transaction completed successfully.                   |
| `transaction.failed`  | A transaction failed.                                   |
| `transaction.pending` | A transaction is awaiting processing or approval.       |
| `wallet.credit`       | A customer or merchant wallet received funds.           |
| `wallet.debit`        | A customer or merchant wallet was debited.              |
| `customer.created`    | A new customer account was created under your merchant. |
| `card.linked`         | A card was linked to a customer wallet.                 |
| `transfer.completed`  | A fund transfer completed successfully.                 |
| `transfer.failed`     | A fund transfer failed.                                 |
| `*`                   | Wildcard — receives all events.                         |

### Legacy events

These event types are preserved for backward compatibility with existing integrations. New integrations should prefer the standardized events above.

| Event                                        | Description                          |
| -------------------------------------------- | ------------------------------------ |
| `nip_debit`                                  | NIP debit occurred.                  |
| `card_debit`                                 | Card debit occurred.                 |
| `pos_withdrawal`                             | POS withdrawal occurred.             |
| `card_debit_reversal`                        | Card debit was reversed.             |
| `remita_fund_transfer`                       | Remita fund transfer initiated.      |
| `remita_fund_transfer_reversal`              | Remita transfer reversed.            |
| `stamp_duty`                                 | Stamp duty applied.                  |
| `bank_transfer`                              | Bank transfer initiated.             |
| `bank_transfer_reversal`                     | Bank transfer reversed.              |
| `account_funded`                             | Merchant account was funded.         |
| `nip_debit_reversal`                         | NIP debit reversed.                  |
| `nip_credit_reversal`                        | NIP credit reversed.                 |
| `wallet_to_wallet_transfer`                  | Wallet-to-wallet transfer completed. |
| `customer_bank_transfer`                     | Customer bank transfer initiated.    |
| `customer_bank_transfer_reversal`            | Customer bank transfer reversed.     |
| `master_card_qr_payment`                     | Mastercard QR payment processed.     |
| `customer_account_funded`                    | Customer account was funded.         |
| `customer_wallet_credited`                   | Customer wallet credited.            |
| `batch_bank_transfer`                        | Batch bank transfer initiated.       |
| `customer_wallet_debited`                    | Customer wallet debited.             |
| `customer_account_created`                   | Customer account created.            |
| `customer_wallet_batch_credited`             | Batch wallet credit completed.       |
| `customer_wallet_batch_debitted`             | Batch wallet debit completed.        |
| `customer_wallet_batch_debitted_by_customer` | Batch debit initiated by customer.   |
| `max_balance_exceeded`                       | Maximum wallet balance was exceeded. |

***

## Webhook payload structure

All events share the same top-level payload structure:

```json theme={null}
{
  "event": "transaction.success",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "reference": "TXN-20240401-001",
    "amount": 500000,
    "status": "success",
    "currency": "NGN",
    "createdAt": "2024-04-01T10:30:00.000Z"
  }
}
```

The `data` object contains fields specific to the event type. For transaction events, it contains the full transaction record. For wallet events, it contains wallet balance details. For customer events, it contains customer profile fields.

### Example payloads

<AccordionGroup>
  <Accordion title="transaction.success">
    ```json theme={null}
    {
      "event": "transaction.success",
      "data": {
        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "reference": "TXN-20240401-001",
        "type": "DEBIT",
        "status": "success",
        "amount": 500000,
        "fee": 1000,
        "vat": 75,
        "total": 501075,
        "currency": "NGN",
        "category": "BANK_TRANSFER",
        "description": "Transfer to Access Bank",
        "createdAt": "2024-04-01T10:30:00.000Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="wallet.credit">
    ```json theme={null}
    {
      "event": "wallet.credit",
      "data": {
        "walletId": "wallet_001",
        "amount": 2000000,
        "type": "credit",
        "reference": "FUND-20240401-001",
        "balanceBefore": 500000,
        "balanceAfter": 2500000,
        "currency": "NGN",
        "creditedAt": "2024-04-01T09:00:00.000Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="customer.created">
    ```json theme={null}
    {
      "event": "customer.created",
      "data": {
        "id": "cust_abc123",
        "firstName": "Amara",
        "lastName": "Okonkwo",
        "email": "amara@example.com",
        "phone": "08012345678",
        "createdAt": "2024-04-01T08:00:00.000Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="card.linked">
    ```json theme={null}
    {
      "event": "card.linked",
      "data": {
        "cardId": "card_xyz789",
        "cardPan": "****4321",
        "walletId": "wallet_001",
        "accountNumber": "0123456789",
        "linkedAt": "2024-04-01T11:00:00.000Z"
      }
    }
    ```
  </Accordion>

  <Accordion title="test.webhook">
    ```json theme={null}
    {
      "event": "test.webhook",
      "data": {
        "message": "This is a test webhook from TagPay",
        "timestamp": "2024-04-01T10:00:00.000Z",
        "subscriptionId": "sub_e5f6a7b8-c9d0-1234-efab-567890123456",
        "merchantId": "merch_001"
      }
    }
    ```
  </Accordion>
</AccordionGroup>

***

## Signature verification

Every webhook request includes headers you should use to verify that the payload originated from TagPay and has not been tampered with.

### Headers

| Header                | Algorithm   | Description                                                            |
| --------------------- | ----------- | ---------------------------------------------------------------------- |
| `x-glide-signature`   | HMAC-SHA512 | Legacy signature. HMAC of the raw JSON payload using your private key. |
| `x-webhook-signature` | HMAC-SHA256 | New signature. HMAC of `{timestamp}.{payload}` using your private key. |
| `x-webhook-timestamp` | —           | Unix timestamp in seconds when the request was signed.                 |

<Info>
  Your private key is the same key used for API authentication. You can retrieve it from the TagPay dashboard under **API Keys**.
</Info>

### Verifying with Node.js

Use the `x-webhook-signature` header for new integrations. It combines the timestamp with the payload to prevent replay attacks.

```javascript verify-webhook.js theme={null}
const crypto = require("crypto");

function verifyWebhookSignature(req, privateKey) {
  const signature = req.headers["x-webhook-signature"];
  const timestamp = req.headers["x-webhook-timestamp"];

  if (!signature || !timestamp) {
    throw new Error("Missing webhook signature headers");
  }

  // Reject requests older than 5 minutes to prevent replay attacks
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    throw new Error("Webhook timestamp is too old");
  }

  // Reconstruct the signed string: "{timestamp}.{rawPayload}"
  const rawBody = JSON.stringify(req.body);
  const dataToSign = `${timestamp}.${rawBody}`;

  const expectedSignature = crypto
    .createHmac("sha256", privateKey)
    .update(dataToSign)
    .digest("hex");

  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    throw new Error("Invalid webhook signature");
  }

  return true;
}

// Express handler example
app.post("/webhooks/tagpay", express.json(), (req, res) => {
  try {
    verifyWebhookSignature(req, process.env.TAGPAY_PRIVATE_KEY);
  } catch (err) {
    return res.status(401).json({ error: err.message });
  }

  const { event, data } = req.body;

  switch (event) {
    case "transaction.success":
      // handle successful transaction
      break;
    case "wallet.credit":
      // handle wallet credit
      break;
    default:
      // log unhandled event type
  }

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

### Verifying the legacy signature

If you are on the legacy callback URL system, verify using the `x-glide-signature` header and SHA-512:

```javascript verify-legacy-webhook.js theme={null}
const crypto = require("crypto");

function verifyLegacySignature(req, privateKey) {
  const signature = req.headers["x-glide-signature"];

  if (!signature) {
    throw new Error("Missing x-glide-signature header");
  }

  const rawBody = JSON.stringify(req.body);

  const expectedSignature = crypto
    .createHmac("sha512", privateKey)
    .update(rawBody)
    .digest("hex");

  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    throw new Error("Invalid webhook signature");
  }

  return true;
}
```

<Warning>
  Always use `crypto.timingSafeEqual` when comparing signatures. Standard string equality (`===`) is vulnerable to timing attacks.
</Warning>

***

## Retry policy

TagPay attempts delivery up to **3 times** per event before marking the delivery as `failed`.

| Attempt | Timing                              |
| ------- | ----------------------------------- |
| 1       | Immediately after the event fires.  |
| 2       | 1 minute after the first failure.   |
| 3       | 5 minutes after the second failure. |

After all attempts are exhausted, you can manually trigger another delivery using the [replay endpoint](/api/webhooks/subscriptions#replay-a-failed-delivery).

<Tip>
  Your endpoint must respond with a `2xx` HTTP status code within **10 seconds** for the delivery to be counted as successful. Responses that time out or return `3xx`, `4xx`, or `5xx` status codes are treated as failures and trigger a retry.
</Tip>
