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

# Webhooks

> Receive final purchase status updates.

Webhooks are sent only for purchases that return `processing`. Use `data.reference` to match the final result to your order.

## Events

| Event                    | Description                      |
| ------------------------ | -------------------------------- |
| `WEBHOOK_TEST`           | Endpoint verification request.   |
| `TRANSACTION_SUCCESSFUL` | Purchase completed successfully. |
| `TRANSACTION_FAILED`     | Purchase failed.                 |

## Endpoint verification

Datashop sends a signed `WEBHOOK_TEST` request before saving an endpoint. Return any `2xx` response to accept it.

## Headers

```http theme={null}
Webhook-Id: evt_12345
Webhook-Timestamp: 1788000000
Webhook-Signature: v1,BASE64_SIGNATURE
X-Datashop-Event: TRANSACTION_SUCCESSFUL
X-Datashop-Reference: merchant-ref-8905fff6767
```

## Payload

```json theme={null}
{
  "event": "TRANSACTION_SUCCESSFUL",
  "message": "Purchase processed successfully.",
  "data": {
    "reference": "merchant-ref-8905fff6767",
    "customer_id": "45705225097",
    "service": "electricity",
    "product_name": "jos-electric-prepaid",
    "quantity": "1000",
    "provider": "Jos Electric",
    "paid_amount": 990,
    "balance_before": 12500,
    "balance_after": 11510,
    "transaction_status": "successful",
    "remark": "7060-8360-1787-0933-6869\n\n(4.5 KWH)",
    "token": "7060-8360-1787-0933-6869",
    "units": "4.5",
    "meter_number": "45705225097",
    "customer_name": "UMAR MOHD AWWAL",
    "customer_address": "NEW MILE 3",
    "disco": "Jos Electric - JED",
    "transaction_date": "2026-08-02T09:57:35.000000Z"
  }
}
```

## Verify signatures

Datashop signs this string:

```txt theme={null}
Webhook-Id.Webhook-Timestamp.raw_body
```

Verify it with the webhook signing secret from your Business console.

```javascript theme={null}
import crypto from "node:crypto";

const secret = "whsec_...";
const webhookId = request.headers["webhook-id"];
const timestamp = request.headers["webhook-timestamp"];
const signatureHeader = request.headers["webhook-signature"];
const rawBody = request.rawBody;

const signedPayload = `${webhookId}.${timestamp}.${rawBody}`;
const expected = crypto
  .createHmac("sha256", secret)
  .update(signedPayload)
  .digest("base64");

const received = String(signatureHeader || "").replace("v1,", "");

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

Reject old timestamps and ignore duplicate `Webhook-Id` values.

## Retries

Return a `2xx` response after accepting the event. Datashop retries failed deliveries, so process each event idempotently.
