A single REST API for airtime, data, cable TV, electricity, international top-ups, gift cards and travel eSIMs. All requests and responses are JSON, authenticated with your API keys, and every call returns the same response envelope. Failed orders are reversed to your wallet automatically.
https://globconnects.net/api/v1In Settings → API keys. You receive a public and a secret key.
Every sale is charged to your Globconnects wallet balance.
Verify with GET /ping, then transact with POST /purchase.
Every request must include your public and secret keys. Choose one of two schemes. Both are equivalent.
Authorization: Bearer PUBLIC_KEY:SECRET_KEY
X-Public-Key: PUBLIC_KEY X-Secret-Key: SECRET_KEY
Keys are created and revoked in Settings → API keys. Keep the secret key server-side; never expose it in client code. All endpoints are relative to the base URL:
https://globconnects.net/api/v1
API keys can be scoped with granular permissions, IP address allowlists, and automatic expiration to minimize blast radius in the event of credential leakage.
| Scope | Grants Access To |
|---|---|
read:balance | Account verification (/ping) and wallet balance lookup (/balance). |
read:catalog | Catalog queries (/services, /products, airtime/data operators, eSIM packages, gift cards). |
write:purchase | Mutating purchase endpoints (airtime, data, cable, electricity, eSIMs, gift cards). |
read:cards | Virtual card listing, balance, and transaction history (/cards, /cards/{ref}, /cards/{ref}/balance, /cards/{ref}/transactions). |
write:cards | Virtual card creation, funding, withdrawal, and freezing (/cards/create, /cards/fund, /cards/withdraw, /cards/freeze). |
cards:reveal | Sensitive card details (/cards/{ref}/reveal). Delivers live PAN & CVV over encrypted TLS. |
read:transactions | Transaction history, detail lookups, and eSIM profile statuses. |
198.51.100.4, 203.0.113.0/24).401 Unauthorized.To safely retry purchase requests without creating double debits or accidental duplicate transactions, pass a unique Idempotency-Key header (e.g. UUIDv4) with every mutating request (POST /purchase, POST /cards/*, POST /esim/*, POST /airtime/purchase).
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d
Idempotency-Replay: true header and zero additional wallet debit.409 Conflict until the initial transaction has settled.Responses share one envelope. On success, status is true and the result is in data. On failure, status is false with a human-readable message, and data is null.
{
"status": true, // false on error
"message": "…", // human-readable summary
"data": { … } // result, or null on error
}
All v1 endpoints at a glance.
| Method | Endpoint | Description |
|---|---|---|
| GET | /ping | Verify API keys |
| GET | /balance | Wallet balance |
| GET | /services | Full catalogue |
| GET | /products/{category} | Products in a category |
| POST | /purchase | Buy airtime, data, cable, power… |
| GET | /transactions/{reference} | Retrieve a transaction |
| GET | /airtime/operators | International airtime operators |
| POST | /airtime/purchase | Send airtime abroad |
| GET | /data/operators | International data operators |
| POST | /data/purchase | Send data abroad |
| GET | /giftcards | Gift-card products |
| POST | /giftcards/purchase | Buy a gift card |
| GET | /esim/packages | eSIM plans for a destination |
| POST | /esim/purchase | Buy an eSIM |
| GET | /esim/status/{reference} | Retrieve an eSIM profile |
| GET | /cards | List virtual USD cards business KYC |
| POST | /cards/create | Issue a virtual USD card business KYC |
| POST | /cards/fund | Top up a card business KYC |
| POST | /cards/freeze | Freeze or unfreeze a card business KYC |
| GET | /cards/{reference} | Retrieve one card business KYC |
| GET | /cards/{reference}/transactions | Card transaction ledger business KYC |
| POST | /cards/reveal | Full PAN, CVV & AVS billing business KYC |
| POST | /cards/{reference}/reveal | Reveal card via URL param business KYC |
Confirms your API keys are valid. Does not touch your wallet.
curl https://globconnects.net/api/v1/ping \ -H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY"
Returns your current wallet balance.
curl https://globconnects.net/api/v1/balance -H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY"
{ "status": true, "data": { "wallet_balance": 15000.00, "currency": "NGN" } }
Returns the full catalogue as categories → products → variations. Use a variation's code as the variation_code when creating a purchase.
{ "status": true, "data": [
{ "code": "data-gifting", "service_type": "data", "products": [
{ "code": "data-gifting-mtn", "network": "MTN", "variations": [
{ "code": "mtn-1gb-30", "price": 600, "data_value": "1GB" }
] } ] } ] }
Returns the products in a single category.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
category |
string | required | Category code, e.g. airtime, data-gifting, cable, electricity, education. |
curl https://globconnects.net/api/v1/products/airtime -H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY"
Creates a purchase for any local service. Manual-amount services (airtime, electricity) also require amount.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
variation_code |
string | required | The variation to buy, from /services. |
recipient |
string | required | Phone, smartcard, meter or customer ID for the service. |
amount |
number | optional | Required for manual-amount services (airtime, electricity). |
meter_type |
string | optional | Electricity only. Use prepaid or postpaid. |
subscription_type |
string | optional | Cable only. Use change or renew. |
curl -X POST https://globconnects.net/api/v1/purchase \
-H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "variation_code": "mtn-1gb-30", "recipient": "08012345678" }'
<?php
$ch = curl_init('https://globconnects.net/api/v1/purchase');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer PUBLIC_KEY:SECRET_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'variation_code' => 'mtn-1gb-30',
'recipient' => '08012345678',
]),
]);
$res = json_decode(curl_exec($ch), true);
echo $res['data']['reference'];
// Server-side Node.js (v18+) — never expose API keys in browser client JavaScript!
const res = await fetch('https://globconnects.net/api/v1/purchase', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.GLOBCONNECTS_PUBLIC_KEY}:${process.env.GLOBCONNECTS_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
variation_code: 'mtn-1gb-30',
recipient: '08012345678',
}),
});
const { data } = await res.json();
console.log(data.reference, data.status);
{ "status": true, "message": "Transaction successful.", "data": {
"reference": "GC240717ABC123", "status": "successful",
"amount": 600, "wallet_balance": 14400.00 } }
Retrieves a transaction by reference. Use it to resolve orders that returned pending.
Path parameters
| Parameter | Type | Description | |
|---|---|---|---|
reference |
string | required | The reference returned when the transaction was created. |
curl https://globconnects.net/api/v1/transactions/GC240717ABC123 \ -H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY"
Top up mobile numbers in 150+ countries. Airtime is priced live per country, so list operators first, then buy. Each operator is either FIXED (choose an options[] entry and send its sender value) or RANGE (send any amount between range.min and range.max). The ngn field is what your wallet is charged.
Opaque tokens: the id and code values here (and for data, gift cards and eSIMs) are opaque. Send them back exactly as received; do not parse or construct them.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
country |
string | required | ISO-3166 alpha-2 country code, e.g. US, GB, GH. |
curl "https://globconnects.net/api/v1/airtime/operators?country=US" \ -H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY"
{ "status": true, "data": {
"country": "US", "operators": [
{ "id": "~mJ8x2", "name": "AT&T USA", "currency": "USD",
"denominationType": "RANGE",
"range": { "min": 5, "max": 100, "ngnMin": 8400, "ngnMax": 168000 },
"options": [] } ] } }
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
operator_id |
string | required | Operator id from /airtime/operators. |
amount |
number | required | A FIXED option's sender value, or any value within the RANGE. |
phone |
string | required | Recipient number in international format, e.g. +14155550123. |
country |
string | required | ISO alpha-2 country code. |
curl -X POST https://globconnects.net/api/v1/airtime/purchase \
-H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "operator_id": "~mJ8x2", "amount": 10,
"phone": "+14155550123", "country": "US" }'
A pending status settles shortly. Poll /transactions/{reference} to confirm.
Data bundles for numbers abroad, using the same live list-then-buy model as airtime. Operators return real bundle sizes in their options[] labels.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
country |
string | required | ISO alpha-2 country code. |
{ "status": true, "data": {
"country": "US", "operators": [
{ "id": "~kQ2pR", "name": "T-Mobile Data USA", "currency": "USD",
"denominationType": "FIXED",
"options": [ { "sender": 10, "ngn": 16800, "label": "3GB - 30 days" } ] } ] } }
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
operator_id |
string | required | Operator id from /data/operators. |
amount |
number | required | The chosen option's sender value. |
phone |
string | required | Recipient number in international format. |
country |
string | required | ISO alpha-2 country code. |
curl -X POST https://globconnects.net/api/v1/data/purchase \
-H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "operator_id": "~kQ2pR", "amount": 10,
"phone": "+14155550123", "country": "US" }'
Sell branded gift cards priced live per country. The card is delivered by email to the recipient.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
country |
string | required | ISO alpha-2 country code. |
{ "status": true, "data": {
"country": "US", "products": [
{ "id": "~7ZgT", "name": "Amazon US", "brand": "Amazon",
"denominationType": "FIXED",
"options": [ { "sender": 25, "ngn": 42000 } ] } ] } }
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
product_id |
string | required | Product id from /giftcards. |
amount |
number | required | A FIXED sender value, or a value within the RANGE. |
recipient_email |
string | required | Where the gift-card code is sent. |
curl -X POST https://globconnects.net/api/v1/giftcards/purchase \
-H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "product_id": "~7ZgT", "amount": 25,
"recipient_email": "friend@example.com" }'
Global travel-data eSIMs, priced live in NGN. The flow is three steps: list plans for a destination, buy one, then fetch its QR / activation profile. Ordering is asynchronous. If the profile isn't ready, the eSIM returns PROCESSING; poll the status endpoint for it.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
region |
string | required | ISO alpha-2 country code (US, GB), !GL for a global plan, or a region code. |
{ "status": true, "data": {
"region": "US", "packages": [
{ "code": "~ifv-1qVTi6TN", "name": "USA 1GB 7 Days",
"data": "1GB", "days": 7, "ngn": 2500.00 } ] } }
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
package_code |
string | required | The code from /esim/packages. |
email |
string | required | Customer email. The QR and receipt are sent here. |
{ "status": true, "message": "eSIM ordered. View the QR to install.", "data": {
"reference": "GC240717ESIM01", "status": "successful", "amount": 2500,
"esim": {
"plan": "USA 1GB 7 Days", "iccid": "8910300000...",
"activation_code": "LPA:1$smdp.io$MATCHING-ID",
"qr_image": "https://.../qr.png", "smdp": "smdp.io",
"status": "GOT_RESOURCE" },
"wallet_balance": 11900.00 } }
To install: scan qr_image, or enter activation_code manually (Settings → Add eSIM).
Re-fetches the QR / activation profile. Use it when a purchase returned status: PROCESSING.
curl https://globconnects.net/api/v1/esim/status/GC240717ESIM01 \ -H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY"
These endpoints require a verified business account. Unlike the rest of the API, an API key alone is not enough. A card is a spendable instrument on the open card networks, so we verify who is behind the account first. Two things must be true:
Until both pass, every endpoint below answers 403 with a machine-readable
reason and the outstanding steps, so you can surface the right
message to your own user:
{ "status": false,
"message": "Virtual cards need a verified business account.",
"data": {
"reason": "BUSINESS_NOT_SUBMITTED",
"steps": [ "Submit your CAC certificate, TIN and director's ID …" ] } }
Reason codes
| Reason | Meaning |
|---|---|
IDENTITY_NOT_VERIFIED | BVN not yet verified on the account. |
BUSINESS_NOT_SUBMITTED | No incorporation documents on file. |
BUSINESS_REVIEW_PENDING | Documents submitted, awaiting review. No action needed. |
BUSINESS_REJECTED | Documents declined. The reason is shown in Settings; correct and resubmit. |
IDENTITY_NOT_ON_FILE | Returned by /cards/create when the account has no verified BVN retained to issue against. |
Cards are issued in USD and funded from your Naira
wallet at the rate quoted by GET /cards (ngn_per_unit, which
already includes the margin). All amount values are in USD.
Every card on the account, plus the current FX quote and funding limits.
{ "status": true, "data": {
"currency": "USD", "ngn_per_unit": 1680.00,
"min_fund": 5, "max_fund": 1000,
"cards": [
{ "reference": "CRD250822AB12CD", "label": "Ads card",
"brand": "Visa", "currency": "USD", "last4": "4821",
"expiry_month": "07", "expiry_year": "2029",
"name_on_card": "ADA OKAFOR", "status": "active",
"balance": 25.00 } ] } }
Issues a dedicated USD virtual card. Each card created via API is issued directly to your customer using their verified individual KYC details, creating an independent cardholder identity. Strict Policy: Each BVN and NIN is eligible for only one virtual card across the platform.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
first_name |
string | required | Customer first name (must match BVN record). |
last_name |
string | required | Customer last name (must match BVN record). |
phone |
string | required | Customer mobile phone number. |
email |
string | required | Customer email address. |
bvn |
string | required | 11-digit Bank Verification Number of the customer. |
nin |
string | required | 11-digit National Identity Number of the customer (must be distinct from BVN). |
date_of_birth |
string | required | Customer date of birth, YYYY-MM-DD. |
street |
string | required | Customer street address. |
city |
string | required | Customer city. |
state |
string | required | Customer state. |
country |
string | optional | ISO alpha-2. Defaults to NG. |
zip_code |
string | required | Customer postal / zip code. |
amount |
number | required | Initial funding amount in USD (minimum $1). |
label |
string | optional | Card label / nickname, e.g. John Online Card. |
curl -X POST https://globconnects.net/api/v1/cards/create \
-H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Joe",
"last_name": "Doe",
"email": "joe.doe@example.com",
"phone": "080******78",
"bvn": "222******55",
"nin": "746******19",
"date_of_birth": "1995-04-12",
"street": "12 Commercial Avenue",
"city": "Yaba",
"state": "Lagos",
"country": "NG",
"zip_code": "101212",
"amount": 5.00,
"label": "Joe Doe Card"
}'
Returns 201 with the newly issued card and your
updated wallet_balance. If card issuance fails, the debited funds are reversed to your wallet automatically.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
card_reference |
string | required | The card’s reference (can be passed in request JSON body or directly in the URL path). |
amount |
number | required | USD to add, between min_fund and max_fund. |
A frozen, expired or terminated card cannot be funded. Unfreeze it first. Repeated failures auto-freeze a card to protect the account.
Unloads USD from an active virtual card and credits the equivalent converted Naira directly into your main account wallet.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
card_reference |
string | required | The card’s reference (can be passed in request JSON body or directly in the URL path). |
amount |
number | required | USD amount to withdraw from the card. |
{
"status": true,
"message": "Card funds withdrawn successfully.",
"data": {
"card": {
"reference": "CRD261010125217A300784E",
"label": "Ads card",
"currency": "USD",
"balance": 10.00,
"status": "active"
},
"wallet_balance": 485000.00
}
}
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
card_reference |
string | required | The card’s reference (can be passed in request JSON body or directly in the URL path). |
frozen |
boolean | optional | true to freeze (default), false to unfreeze. |
Fetches real-time live balance and current status directly from the card network issuer.
{
"status": true,
"message": "Card balance fetched.",
"data": {
"card_reference": "CRD261010125217A300784E",
"currency": "USD",
"balance": 25.00,
"status": "active",
"updated_at": "2026-10-21T16:30:00+01:00"
}
}
One card, in the same shape as the list endpoint. 404 if the reference isn't yours.
Fetches real-time transaction ledger, declines, and settlement history for this card.
Body parameters (for POST /cards/reveal)
| Parameter | Type | Description | |
|---|---|---|---|
card_reference |
string | required | The card’s reference (e.g. CRD261010...). Can alternatively be passed directly in the URL path. |
Full PAN, CVV, and Address Verification System (AVS) billing details, fetched live from the card network issuer. Never stored by us, and
never returned by any other endpoint. Requires cards:reveal (or *) API scope and Business KYC. Response is sent with Cache-Control: no-store, max-age=0, private and Pragma: no-cache.
{
"status": true,
"message": "Card details fetched.",
"data": {
"card": {
"card_reference": "CRD261010125217A300784E",
"pan": "4821000000004821",
"cvv": "123",
"expiry_month": "07",
"expiry_year": "2029",
"name_on_card": "ADA OKAFOR",
"brand": "visa",
"currency": "USD",
"status": "active",
"balance": 25.00,
"billing_address": {
"street": "332 S Michigan Ave Ste 121",
"city": "Chicago",
"state": "IL",
"postal_code": "60604",
"country": "US"
}
}
}
}
Rate limited to 10 calls per hour per
account. A 429 carries retry_after in seconds. Every
call is audit-logged. Treat the response as cardholder data: do not log it, do not cache
it, and serve it to your user over TLS only.
Orders that come back pending settle later. Rather than polling
/transactions/{reference}, set a webhook URL in
Settings → Webhook and we will POST the update to you
as soon as it happens.
Supported Webhook Events
| Event | Category | Triggered when |
|---|---|---|
transaction.successful | Vending | The order was delivered. Any commission has been credited to your wallet. |
transaction.reversed | Vending | The order failed or was refunded. The amount is back in your wallet. |
card.created | Virtual Cards | A new USD virtual card is successfully issued to your customer. |
card.funded | Virtual Cards | A card top-up or reload succeeds and updates the card balance. |
card.withdrawn | Virtual Cards | Funds are withdrawn from a card back to your main account wallet. |
card.transaction | Virtual Cards | A settled spend or online merchant purchase occurs on a card. |
card.declined | Virtual Cards | A transaction attempt on a card is declined (e.g. insufficient balance). |
card.frozen | Virtual Cards | A card is frozen via API or auto-frozen after repeated failed attempts. |
card.unfrozen | Virtual Cards | A frozen card is re-activated. |
funding.successful | Wallet Funding | A bank transfer to your virtual account or online gateway deposit settles. |
funding.failed | Wallet Funding | A wallet deposit attempt fails or is declined. |
Only real state changes are sent. A pending order that is
still pending produces nothing, and a repeated refund is not re-sent. You will never receive duplicate
events for identical actions.
Request we send
Content-Type: application/json
Accept: application/json
User-Agent: Globconnects-Webhook/1.1
X-Globconnects-Signature: t=1725700000,v1=9f2c3d4e5f6a... (HMAC-SHA512 of timestamp.event_id.raw_body)
X-Globconnects-Timestamp: 1725700000
X-Globconnects-Event-Id: evt_9f2c3d4e5f6a7b8c
{
"id": "evt_9f2c3d4e5f6a7b8c",
"event": "transaction.successful",
"timestamp": 1725700000,
"sent_at": "2026-08-07T09:14:22+00:00",
"data": {
"reference": "GC240717ABC123",
"status": "successful",
"service": "data-gifting",
"product": "mtn",
"plan": "gifting-mtn-1gb",
"network": "MTN",
"recipient": "08012345678",
"amount": 480.00,
"commission": 12.00,
"channel": "api",
"provider_reference": "AS-99182734",
"message": "Delivered successfully.",
"created_at": "2026-08-07 09:14:02"
}
}
Payload fields
| Parameter | Type | Description | |
|---|---|---|---|
id |
string | required | Unique event ID (e.g. evt_...). Matches the X-Globconnects-Event-Id header. |
event |
string | required | One of the events above (e.g. transaction.successful). |
timestamp |
integer | required | Unix epoch timestamp when the webhook was generated. |
sent_at |
string | required | ISO-8601 UTC timestamp of this delivery attempt. |
data.reference |
string | required | Your order reference. Match this against your own records. |
data.status |
string | required | successful or reversed. Always the settled value. |
data.amount |
number | required | Amount charged, in NGN. |
data.commission |
number | required | Commission earned. Reversed if the order is later refunded. |
data.provider_reference |
string | optional | Upstream reference, when the provider supplies one. |
data.message |
string | optional | Human-readable outcome. |
Responding
Reply 2xx to acknowledge. Anything else is recorded as a failed delivery.
We wait up to 5 seconds for your response, do not follow redirects, and
strictly require secure HTTPS URLs (plain HTTP and private/internal IP ranges are blocked for SSRF protection).
Every outbound webhook carries security headers to prove authenticity and eliminate replay attacks:
X-Globconnects-Signature: Formatted as t=<timestamp>,v1=<signature> where the signature is HMAC-SHA512 of timestamp . "." . event_id . "." . raw_body.X-Globconnects-Timestamp: Unix timestamp (seconds) when the delivery was initiated. Reject any requests older than 5 minutes.X-Globconnects-Event-Id: Unique event ID (e.g. evt_...). Integrators must verify this matches the id field in the JSON payload body.<?php
$secret = getenv('GLOBCONNECTS_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_GLOBCONNECTS_SIGNATURE'] ?? '';
$eventIdHeader = $_SERVER['HTTP_X_GLOBCONNECTS_EVENT_ID'] ?? '';
// 1. Parse timestamp and signature
if (!preg_match('/t=(\d+),v1=([a-f0-9]+)/', $sigHeader, $m)) {
http_response_code(401);
exit('Invalid signature format');
}
[$full, $timestamp, $sig] = $m;
// 2. Replay attack protection (within 5 minutes)
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit('Timestamp expired');
}
$event = json_decode($raw, true);
// 3. Verify header Event-ID matches payload body ID
if ($eventIdHeader === '' || ($event['id'] ?? '') !== $eventIdHeader) {
http_response_code(401);
exit('Event ID mismatch');
}
// 4. Timing-safe HMAC verification over timestamp.event_id.raw_body
$expected = hash_hmac('sha512', $timestamp . '.' . $eventIdHeader . '.' . $raw, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('Signature mismatch');
}
// Look the order up by reference and update your own records.
fulfil($event['data']['reference'], $event['data']['status']);
http_response_code(200);
const crypto = require('crypto');
// express: mount the RAW body for this route, not express.json()
app.post('/hooks/globconnects',
express.raw({ type: 'application/json' }),
(req, res) => {
const sigHeader = req.get('X-Globconnects-Signature') || '';
const eventIdHeader = req.get('X-Globconnects-Event-Id') || '';
const secret = process.env.GLOBCONNECTS_WEBHOOK_SECRET;
const match = sigHeader.match(/t=(\d+),v1=([a-f0-9]+)/);
if (!match) return res.status(401).send('Invalid signature format');
const timestamp = parseInt(match[1], 10);
const signature = match[2];
// Replay protection: within 5 minutes
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) {
return res.status(401).send('Timestamp expired');
}
const payload = JSON.parse(req.body.toString());
// Verify header Event-ID matches payload body ID
if (!eventIdHeader || payload.id !== eventIdHeader) {
return res.status(401).send('Event ID mismatch');
}
// Timing-safe verification over timestamp.event_id.raw_body
const expected = crypto
.createHmac('sha512', secret)
.update(timestamp + '.' + eventIdHeader + '.' + req.body.toString('utf8'))
.digest('hex');
if (expected.length !== signature.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).send('Signature mismatch');
}
fulfil(payload.data.reference, payload.data.status);
res.sendStatus(200);
});
import hmac, hashlib, os, time, re
from flask import Flask, request, abort
app = Flask(__name__)
@app.post('/hooks/globconnects')
def globconnects_hook():
raw = request.get_data().decode('utf-8')
sig_header = request.headers.get('X-Globconnects-Signature', '')
event_id_header = request.headers.get('X-Globconnects-Event-Id', '')
secret = os.environ['GLOBCONNECTS_WEBHOOK_SECRET']
match = re.match(r't=(\d+),v1=([a-f0-9]+)', sig_header)
if not match:
abort(401)
timestamp, signature = int(match.group(1)), match.group(2)
# Replay protection: within 5 minutes
if abs(int(time.time()) - timestamp) > 300:
abort(401)
event = request.get_json()
# Verify header Event-ID matches payload body ID
if not event_id_header or event.get('id') != event_id_header:
abort(401)
signed_payload = f'{timestamp}.{event_id_header}.{raw}'
expected = hmac.new(secret.encode(), signed_payload.encode(), hashlib.sha512).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
fulfil(event['data']['reference'], event['data']['status'])
return '', 200
Deliveries can arrive more than once if an order is settled
and later refunded, so key your handling on data.reference or X-Globconnects-Event-Id and make it idempotent.
Rotate the secret by clearing and re-saving your webhook URL.
The status field on a transaction is one of:
| Status | Meaning |
|---|---|
| successful | Delivered to the recipient. |
| pending | Accepted and settling. Re-check with /transactions/{reference}, or let a webhook tell you. |
| reversed | Failed and automatically refunded to your wallet. |
HTTP status codes
| Code | Meaning |
|---|---|
200 / 201 | Success. |
401 | Missing or invalid API keys. |
402 | Insufficient wallet balance. |
404 | Resource not found. |
409 | Conflict / In-flight operation. A request with the same Idempotency-Key is currently processing. |
422 | Validation error. See message. |
429 | Too many requests. Retry after a short delay. |
502 | Upstream provider timeout or error. Retry only using your original Idempotency-Key to guarantee against double processing. |