Skip to navigation

Webhook

View as Markdown

VTUAgent notifies your server when an airtime or data purchase changes state, so you don’t have to poll.

Register your endpoint

POST /v1/webhook/set
Authorization: Bearer {your_api_key}
Content-Type: application/json
{ "webhook_url": "https://example.com/vtuagent/webhook" }

One URL per account; calling again replaces it. Retrieve the current value with GET /v1/webhook/get. Your endpoint must be a publicly reachable absolute URL — use HTTPS.

Delivery

For each event VTUAgent sends a single POST to your URL with Content-Type: application/json and a 10-second timeout.

  • No retries. If your endpoint is down or responds slowly, the event is not resent. If you miss a delivery, fetch the outcome with POST /v1/transaction/status.
  • No Authorization header. Authenticity is carried by the hash field in the body (see below).
  • Respond with HTTP 200 as quickly as possible and do your own processing afterwards.

Events

eventSent bystatusWhen
airtime.purchase.successPOST /v1/airtime/purchasesuccessProvider confirmed delivery
airtime.purchase.failedPOST /v1/airtime/purchasefailedProvider rejected or was unreachable — wallet refunded
airtime.purchase.pendingPOST /v1/airtime/purchasependingProvider still processing
data.purchase.successPOST /v1/data/purchasesuccessProvider confirmed delivery
data.purchase.failedPOST /v1/data/purchasefailedProvider rejected — wallet refunded
transaction.requeryPOST /v1/transaction/statussuccess, failed, pending, initiated, refundedEvery time you call the status endpoint; mirrors the stored transaction

Cable TV and electricity purchases do not emit webhooks. Their responses are synchronous; if one returns 202 pending, poll POST /v1/transaction/status.

Payload

The body always contains exactly these eight fields:

{
"event": "data.purchase.success",
"reference": "202609131244GPTFV",
"status": "success",
"amount": "1454.98",
"product_type": "DATA",
"details": "Data purchase successful - 5.5GB 2-Days for 07041747115",
"hash": "5d6a5ec98b7f07a4d0e09cd8016d0619eb8c162ede52a5319cd59a13ddf442a5",
"timestamp": "2026-09-13T11:44:26.938764932Z"
}
FieldTypeDescription
eventstringOne of the events above
referencestringThe request_ref you sent when purchasing
statusstringLowercase outcome — see the events table
amountstringNaira, decimal string
product_typestringAIRTIME, DATA, CABLE_TV, ELECTRICITY
detailsstringHuman-readable outcome. Same text as the API response message; for transaction.requery it is the stored transaction description
hashstringAuthenticity digest — 64 lowercase hex characters (see below)
timestampstringRFC 3339, UTC, nanosecond precision

Verifying the hash

Every webhook carries a SHA-256 digest of your API key and the transaction reference, joined with a colon. Because only VTUAgent and you know the API key, a matching digest confirms the webhook came from VTUAgent and refers to a transaction on your account.

hash = hex( SHA-256( "{your_api_key}:{reference}" ) )

Recompute it and compare to the received value with a constant-time comparison. Reject the request if they differ.

import hashlib, hmac
def is_valid(payload: dict, api_key: str) -> bool:
expected = hashlib.sha256(f"{api_key}:{payload['reference']}".encode()).hexdigest()
return hmac.compare_digest(expected, payload["hash"])
const { createHash, timingSafeEqual } = require("node:crypto");
function isValid(payload, apiKey) {
const expected = createHash("sha256").update(`${apiKey}:${payload.reference}`).digest("hex");
return expected.length === payload.hash.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(payload.hash));
}
$expected = hash('sha256', $apiKey . ':' . $payload['reference']);
$valid = hash_equals($expected, $payload['hash']);

If you have more than one active API key, the hash is computed with the most recently created one — update your webhook handler when you rotate keys.

Handling duplicates

transaction.requery fires on every call to the status endpoint, so you may receive the same reference more than once, and a pending event is normally followed by success or failed. Key your processing on reference and apply state transitions idempotently — never move a transaction from success back to pending, and never fulfil the same reference twice.

Checklist

  1. Register an HTTPS URL with POST /v1/webhook/set.
  2. On each delivery: parse the JSON, verify hash, respond 200 immediately.
  3. Process asynchronously, matching reference to your own records.
  4. Handle repeats idempotently; there are no retries, so use POST /v1/transaction/status to recover any missed event.