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

# Webhooks

> Receive real-time server notifications when events occur on your account.

**Ideal for**: Reacting to payments automatically — marking orders paid, crediting wallets, releasing goods — without polling for a result.

> **Webhooks or verification?** Webhooks push each outcome to you as it happens; [verifying a payment](/online-payments/after-payments/verify-payment) lets you ask on demand. Most integrations use both — see the [comparison](/online-payments/after-payments/verify-payment#verify-or-webhooks).

> **Taking payments on a POS terminal?** In-store transactions use a separate webhook system with its own headers, event names and payload format — see [in-store webhooks](/in-store/webhooks). This page covers online payments.

Webhook Events enable real-time communication between SeerBit and your application. When specific activities occur within the payment ecosystem such as transactions, refunds, wallet movements, disputes, or virtual account events, SeerBit sends a structured notification to your configured webhook endpoint.<br />

Webhooks use a standardized acknowledgment protocol designed to ensure reliable delivery, traceability, and idempotent handling.

## 1. Overview

A webhook is an HTTP callback: a POST request sent to your server when an event happens. When your webhook URL is configured in SeerBit, the platform automatically notifies your system whenever a subscribed event occurs.

##### When your endpoint receives a webhook request:

1. Your server must respond with a `2xx` status code
2. Your server must return a JSON acknowledgment object
3. SeerBit considers the event delivered upon receiving a valid acknowledgment
4. Your server processes the event

If the acknowledgment is missing, invalid, or delayed, SeerBit retries delivery.

## 2. Setting up your webhook

### Expose an endpoint

SeerBit delivers events as HTTP POST callbacks, so you need a server that:

1. Exposes an endpoint able to receive JSON requests over HTTPS.
2. Is reachable from the public internet. Depending on your network and security requirements, you may also need to add SeerBit's IP addresses to your firewall allowlist — ask your account team for the current list.

### Subscribe to notifications

1. Log in to your [SeerBit dashboard](https://dashboard.seerbit.com/#/auth/login).
2. Navigate to **Account Settings > Webhook**.
3. Enter your notification URL.
4. Click **Subscribe**.

Configure the URL separately in test and live mode — a test-mode subscription does not carry over when you go live.

## 3. Delivery Architecture

* **Event Triggered**<br />
  A payment, refund, settlement, dispute, or wallet event occurs.

* **Webhook Preparaion**<br />
  SeerBit constructs the event payload and generates an acknowledgment reference, sent as the `X-Expected-Ack-Reference` header.

* **Delivery Attempt**<br />
  A POST request is sent to your Webhook URL over HTTPS.

* **Merchant Processing**<br />
  Your server receives the request and process the event.

* **Acknowledgment Response**<br />
  Your server returns:

```json theme={null}
{
  "ackReference": "...",
  "status": "processed"
}
```

* **Confirmation**<br />
  SeerBit mark the Webhook as delivered.

* **Retry (if acknowledgment fails)**<br />
  Multiple retries occur at increasing intervals.

This handshake ensures that events are not lost and can be processed reliably and idempotently.

## 4. Webhook Request Format

HTTP Method - <Badge color="blue">POST</Badge><br />
Content-Type - `application/json`

| Header | Type | Description | Required |
| - | - | - | - |
| Content-Type | `string` | Always application/json | Yes |
| X-Expected-Ack-Reference | `string` | A unique reference for this delivery. You must return it in the acknowledgment response. | Yes |
| X-Webhook-Signature | `string` | HMAC-SHA256 signature of the request body, in the form `sha256=<signature>`. See [Verifying signatures](#verifying-signatures). | Yes |
| X-Webhook-Encrypted | `string` | `true` when the body is encrypted. See [Decrypting payloads](#decrypting-payloads). | No |

Body - A JSON object containing event metadata and associated data specific to the event type. If payload encryption is enabled for your business, the body is instead a Base64 string — see [Decrypting payloads](#decrypting-payloads).

## 5. Acknowledgment Response (Required)

To confirm receipt of a webhook event, your server must return:

* Any `2xx` status code (`200 OK` is typical)
* A JSON Object
* Within 5 seconds

```json Acknowledgement JSON Structure theme={null}
{
  "ackReference": "<value from X-Expected-Ack-Reference>",
  "status": "processed"
}
```

##### Field Definitions

| Field | Type | Description |
| - | - | - |
| ackReference | `string` | Must match the value SeerBit sent in the `X-Expected-Ack-Reference` header of that delivery. Echo it back exactly — do not generate your own. |
| status | `string` | Set to `"processed"`. |

SeerBit requires this acknowledgment to validate that your system has successfully accepted the event.

## 6. Retry Logic

A delivery counts as failed when your server:

* Is unreachable, or the connection fails
* Times out (exceeds 5 seconds)
* Returns a status code outside the `2xx` range
* Returns a `2xx` without a valid acknowledgment — `ackReference` is missing, or doesn't match the `X-Expected-Ack-Reference` sent with that delivery

SeerBit decides whether to retry based on the type of failure, so not every failed delivery is retried. Treat retries as a safety net, not a guarantee: if a webhook you're expecting hasn't arrived, [verify the payment](/online-payments/after-payments/verify-payment) rather than waiting for it.

#### Retry schedule

Retries use exponential backoff. The first retry comes about 30 seconds after the failed attempt, and the wait grows with each attempt, up to a maximum of 1 hour between attempts. A delivery is retried up to 5 times by default.

**If all attempts fail, the webhook is marked as undelivered**.

## 7. Event Types

Every payload carries an `eventType`, an `eventDate` and a unique `eventId`. Switch on `eventType` to route the event:

| `eventType` | Event | Fired when |
| - | - | - |
| `transaction` | [Transaction](#7-3-transaction-event) | A payment resolves |
| `transaction` | [Virtual account](#7-7-virtual-account-transaction-event) | Money lands in a [virtual account](/online-payments/payment-features/virtual-account) |
| `transaction.wallet` | [Wallet](#7-4-wallet-transaction-event) | A wallet balance moves |
| `transaction.recurrent` | [Recurrent](#7-5-recurrent-transaction-event) | A [subscription](/online-payments/payment-features/subscription) charge is attempted |
| `transaction.recurring.debit` | [Recurring debit](#7-6-recurring-debit-transaction-event) | A recurring debit is taken |
| `refund` | [Refund](#7-1-refund-event) | A [refund](/online-payments/after-payments/refund) is processed |
| `dispute` | [Dispute](#7-2-dispute-event) | A dispute is raised |

> **Two events share `eventType: "transaction"`.** A standard payment and a virtual account credit both use it, so `eventType` alone will not tell them apart — a virtual account event carries `creditAccountNumber` and `creditAccountName`, which a standard transaction does not. Branch on the payload shape, not the type alone.

Below are the supported webhook events, including the exact sample payloads as used in production today.

### 7.1 Refund Event

The refund event notification body is structured thus:

```json theme={null}
{
  "notificationItems": [
    {
      "notificationRequestItem": {
        "eventType": "refund",
        "eventDate": "2020-05-01 12:55:57",
        "eventId": "0be677f841254a3eb92fab0d0b6ba232",
        "data": {
          "amount": "10",
          "createdAt": "2019-10-24 07:47:49",
          "transactionRef": "IHrE1571828556059",
          "description": "I need my money",
          "type": "FULL_REFUND",
          "mode": "TEST",
          "updatedAt": "2019-10-24 07:47:49"
        }
      }
    }
  ]
}
```

### 7.2 Dispute Event

The dispute event notification body is structured thus:

```json theme={null}
{
  "notificationItems": [
    {
      "notificationRequestItem": {
        "eventType": "dispute",
        "eventDate": "2020-05-01 12:56:07",
        "eventId": "da28df9ea5dd4807b59e5761afd7231b",
        "data": {
          "evidence": [
            {
              "images": [{ "image": "" }]
            }
          ]
        }
      }
    }
  ]
}
```

### 7.3 Transaction Event

The transaction event notification body is structured thus

```json theme={null}
{
  "notificationItems": [
    {
      "notificationRequestItem": {
        "eventType": "transaction",
        "eventDate": "2024-07-01 08:56:16",
        "eventId": "e1c98e0ba9364843b7fa8bd8df0e3bc1",
        "data": {
          "amount": 922.63,
          "mobile": "404",
          "reference": "SBT-T19824129237",
          "gatewayMessage": "Successful",
          "publicKey": "SBTESTPUBK_vtI76q2HWA7QHPbqC1M88HC89gllHKfE",
          "businessName": "opeyemi test",
          "fee": "13.63",
          "productId": "",
          "channelType": "MASTERCARD",
          "maskedPan": "5123-45xx-xxxx-0008",
          "sourceIP": "154.113.161.130",
          "deviceType": "Desktop",
          "fullname": "ope seun",
          "email": "seunopeyemi16@gmail.com",
          "gatewayReference": "SEERLFN8WWTSILB7ZG7",
          "country": "NG",
          "currency": "NGN",
          "narration": "",
          "createdAt": "2024-07-01T08:56:07",
          "updatedAt": "2024-07-01T08:56:13.574",
          "lastFourDigits": "0008",
          "cardBin": "512345",
          "reason": "Successful",
          "paymentType": "CARD",
          "gatewayCode": "00",
          "code": "00"
        }
      }
    }
  ]
}
```

### 7.4 Wallet Transaction Event

The wallet transaction event notification body is structured thus

```json theme={null}
{
  "notificationItems": [
    {
      "notificationRequestItem": {
        "eventType": "transaction.wallet",
        "eventDate": "2020-05-01 12:52:28",
        "eventId": "c472deceabf44924901b104523af14df",
        "data": {
          "amount": "100.00",
          "mobile": "08033456500",
          "reference": "shh3332hwhwhh22hjjjwj",
          "gatewayMessage": "APPROVED",
          "publicKey": "hhw33y2x",
          "bankCode": "000016",
          "description": "wallet transaction",
          "fee": "2.00",
          "type": "Transfer",
          "fullname": "John Doe",
          "email": "peter.diei@centricgateway.com",
          "country": "NG",
          "currency": "NGN",
          "origin": "string",
          "internalRef": "string",
          "creditAccountName": "Test Account",
          "creditAccountNumber": "23221122321",
          "originatorAccountnumber": "1929383828392",
          "originatorName": "CGW",
          "narration": "my narration here",
          "sessionId": "00002999299388837772828883778",
          "externalReference": "2203000002992910219",
          "createdAt": "2019-12-12 16:20:59",
          "updatedAt": "2019-12-12 16:20:59"
        }
      }
    }
  ]
}
```

### 7.5 Recurrent Transaction Event

The recurrent transaction event notification body is structured thus

```json theme={null}
{
  "notificationItems": [
    {
      "notificationRequestItem": {
        "eventType": "transaction.recurrent",
        "eventDate": "2020-05-01 12:50:33",
        "eventId": "30a33df05b0c465c8c38f4113621685a",
        "data": {
          "amount": "150",
          "mobile": "08033456500",
          "reference": "TESTPilotR251218123PPOIU149",
          "publicKey": "SBTESTPUBK_dhrpzbRpR34l6VmqkCFOKA94L5E1jSTu",
          "description": "Pilot Test Subscription",
          "productId": "Terrain",
          "maskedPan": "5123-45xx-xxxx-0008",
          "email": "akintoyekolawole@gmail.com",
          "gatewayReference": "F325090871582705056234",
          "country": "NG",
          "narration": "Reccurrent",
          "createdAt": "2020-02-26T09:17:30",
          "updatedAt": "2020-02-26T09:18:26.496",
          "lastFourDigits": "0008",
          "cardBin": "512345"
        }
      }
    }
  ]
}
```

### 7.6 Recurring Debit Transaction Event

The recurrent debit transaction event notification body is structured thus

```json theme={null}
{
  "notificationItems": [
    {
      "notificationRequestItem": {
        "eventType": "transaction.recurring.debit",
        "eventDate": "2020-05-01 12:55:32",
        "eventId": "799f8cad23bc4bc389280f996d81ea55",
        "data": {
          "amount": "2000",
          "reference": "PILOT76558370651618723659",
          "gatewayMessage": "Successful",
          "publicKey": "SBTESTPUBK_dhrpzbRpR34l6VmqkCFOKA94L5E1jSTu",
          "description": "Authorised charge",
          "channelType": "Recurring Debit",
          "maskedPan": "5123--4xx-xxxx-xx-0",
          "type": "TOKEN",
          "fullname": "Frank Gboyega",
          "email": "gboyega@fcmb.com",
          "gatewayReference": "F786046901582644089560",
          "country": "NG",
          "currency": "NGN",
          "narration": "Authorised charge",
          "createdAt": "2020-02-25T16:21:23",
          "updatedAt": "2020-02-25T16:21:31.319",
          "paymentType": "card"
        }
      }
    }
  ]
}
```

### 7.7 Virtual Account Transaction Event

The virtual account transaction event notification body is structured thus

```json theme={null}
{
  "notificationItems": [
    {
      "notificationRequestItem": {
        "eventType": "transaction",
        "eventDate": "2023-10-06 12:56:47",
        "eventId": "88bf9852405143bd99502c378b316fdd",
        "data": {
          "amount": 100,
          "country": "NG",
          "creditAccountName": "BusinessName(Customer Name)",
          "creditAccountNumber": "4018013418",
          "currency": "NGN",
          "email": "xyx@email.com",
          "externalReference": "GT-012",
          "fullname": "Adamu Bola Ciroma",
          "gatewayCode": "00",
          "code": "00",
          "internalRef": "",
          "gatewayMessage": "Successful",
          "mobile": "404",
          "narration": "",
          "origin": "",
          "originatorAccountnumber": "<Account Number>",
          "originatorName": "<Account Name>",
          "publicKey": "<PublicKey>",
          "reference": "GT-012_SBT_9ADPCIV269",
          "reason": "Successful"
        }
      }
    }
  ]
}
```

## 8. Sample Implementations

Below are full sample implementations demonstrating how to handle webhook acknowledgments.

<CodeGroup>
  ```python Python(FastAPI) theme={null}
  from fastapi import FastAPI, Request, Header
  from fastapi.responses import JSONResponse

  app = FastAPI()

  @app.post("/webhook")
  async def receive_webhook(request: Request, x_expected_ack_reference: str = Header(...)):
      body = await request.body()
      print("Received webhook:", body.decode())

      # Echo the reference SeerBit sent — it is required on every delivery
      return JSONResponse({
          "ackReference": x_expected_ack_reference,
          "status": "processed"
      })
  ```

  ```javascript Node.JS(Express) theme={null}
  const express = require('express');
  const app = express();

  app.use(express.json());

  app.post('/webhook', (req, res) => {
    const body = req.body;
    const ackRef = req.headers['x-expected-ack-reference'];

    console.log('Received webhook:', JSON.stringify(body));

    // Echo the reference SeerBit sent — it is required on every delivery
    res.json({
      ackReference: ackRef,
      status: 'processed',
    });
  });
  ```

  ```java Java(Spring Boot) theme={null}
  @RestController
  public class WebhookController {

      @PostMapping("/webhook")
      public ResponseEntity<?> receiveWebhook(
          @RequestBody String body,
          @RequestHeader("X-Expected-Ack-Reference") String ackRef) {

          System.out.println("Received webhook: " + body);

          // Echo the reference SeerBit sent — it is required on every delivery
          Map<String, String> response = new HashMap<>();
          response.put("ackReference", ackRef);
          response.put("status", "processed");

          return ResponseEntity.ok(response);
      }
  }
  ```
</CodeGroup>

## 9. Security Considerations

Every delivery is signed. Verify the signature before acting on a webhook, and layer the practices below on top of it.

### Verifying signatures

Each delivery carries an `X-Webhook-Signature` header in the form `sha256=<signature>` — an HMAC-SHA256 of the request body. To verify it:

1. Read the **raw** request body, before any JSON parsing — re-serialised JSON won't reproduce the same bytes.
2. Compute an HMAC-SHA256 of the raw body using your webhook signing secret.
3. Compare your result with the value after `sha256=`, using a constant-time comparison.
4. Reject the request if they don't match.

### Decrypting payloads

If your SeerBit account team has enabled payload encryption for your business, the body is encrypted with AES-256-GCM and delivered as a Base64 string, and `X-Webhook-Encrypted` is set to `true`. Check that header, and decrypt the body before parsing it.

| Property | Value |
| - | - |
| Algorithm | AES-256-GCM |
| Key derivation | SHA-256 hash of your SeerBit secret key |
| IV length | 12 bytes, random, prepended to the ciphertext |
| Tag length | 128 bits (16 bytes), appended to the ciphertext |
| Encoding | Base64 of IV + ciphertext + GCM tag |

##### Steps to decrypt

1. Base64-decode the webhook payload body.
2. Take the first 12 bytes as the IV (initialization vector).
3. Take the last 16 bytes as the GCM authentication tag.
4. Derive the AES key as `SHA-256(secretKey)`.
5. Decrypt the remaining bytes (the ciphertext) using AES-256-GCM with the derived key, IV, and tag.

Decryption fails if the payload has been altered or the wrong key is used — reject the request rather than falling back to the raw body.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function decryptWebhook(body, secretKey) {
    const raw = Buffer.from(body, 'base64');
    const iv = raw.subarray(0, 12);
    const tag = raw.subarray(raw.length - 16);
    const ciphertext = raw.subarray(12, raw.length - 16);
    const key = crypto.createHash('sha256').update(secretKey, 'utf8').digest();

    const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
    decipher.setAuthTag(tag);
    const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
    return JSON.parse(plaintext.toString('utf8'));
  }
  ```

  ```python Python theme={null}
  import base64
  import hashlib
  import json

  from cryptography.hazmat.primitives.ciphers.aead import AESGCM


  def decrypt_webhook(body: str, secret_key: str) -> dict:
      raw = base64.b64decode(body)
      iv, ciphertext_and_tag = raw[:12], raw[12:]  # AESGCM expects the tag appended
      key = hashlib.sha256(secret_key.encode("utf-8")).digest()
      plaintext = AESGCM(key).decrypt(iv, ciphertext_and_tag, None)
      return json.loads(plaintext)
  ```
</CodeGroup>

The following practices are strongly recommended:

### Inbound Request Validation

* Enforce HTTPS
* Reject requests not coming from approved IP ranges (if applicable)
* Validate Content-Type = application/json
* Validate the structure of the payload

### Idempotency

Your webhook consumer must ensure duplicate events are not processed twice.

##### Recommended approaches

* Use eventId field as a unique identifier
* Store processed event IDs in a database
* Check before applying business logic

### Logging

Log:

* Full payload
* Ack reference
* Processing time
* Errors and decisions

## 10. Testing Webhooks

##### To test locally

Use tools like:

* ngrok
* cloudflared tunnel
* localtunnel

This exposes your local server to receive webhook events.

##### Replay event

Your internal systems should allow replay during testing, especially for integration and QA teams.

## 11. Troubleshooting

##### Webhook not arriving

* Check that your URL is reachable publicly
* Check SSL certificates
* Verify URL configuration in SeerBit dashboard
* Confirm firewall/NAT rules

##### Webhook arrives but no retries

Your server may be returning a `2xx` status with an incorrect acknowledgment body. The response must contain `ackReference` — echoing the exact value from the `X-Expected-Ack-Reference` header of that delivery — and `status: "processed"`. A reference you generated yourself will not match, and the delivery is treated as unacknowledged.

##### Event processed twice

Implement idempotency based on the eventId field.

##### 400 or parsing errors

Ensure your endpoint expects JSON and does not require authentication unless pre-agreed. If the body is a Base64 string rather than JSON, encryption is enabled for your business — [decrypt it](#decrypting-payloads) first.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.