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 and metadata retrieval (/cards, /cards/{ref}). |
write:cards | Virtual card creation, funding, and freezing (/cards/create, /cards/fund, /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 |
| POST | /cards/{reference}/reveal | Full PAN & CVV 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 card. The cardholder fields are required on the first card and reused
for every card after, so later calls only need amount and label.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
amount |
number | optional | Initial load in USD. Omit or send 0 to issue an empty card. |
label |
string | optional | Your own name for the card, e.g. Ads card. |
date_of_birth |
string | required | Cardholder date of birth, YYYY-MM-DD. |
street |
string | required | Cardholder street address. |
city |
string | required | Cardholder city. |
state |
string | required | Cardholder state. |
country |
string | optional | ISO alpha-2. Defaults to NG. |
zip_code |
string | required | Postal code. |
There is no id_type / id_number
parameter. The cardholder's identity is taken from the verified BVN already on the account,
a number posted on the request is only an assertion, whereas one on file passed a government check against
the account holder's own name. If nothing is on file the call returns 422 with
reason: "IDENTITY_NOT_ON_FILE".
curl -X POST https://globconnects.net/api/v1/cards/create \
-H "Authorization: Bearer PUBLIC_KEY:SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 20, "label": "Ads card",
"date_of_birth": "1990-04-12",
"street": "12 Awolowo Road", "city": "Ikoyi",
"state": "Lagos", "country": "NG", "zip_code": "101233" }'
Returns 201 with the new card and your
updated wallet_balance. If issuing fails after the wallet was debited, the
debit is reversed automatically.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
card_reference |
string | required | The card’s reference. |
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.
Body parameters
| Parameter | Type | Description | |
|---|---|---|---|
card_reference |
string | required | The card’s reference. |
frozen |
boolean | optional | true to freeze (default), false to unfreeze. |
One card, in the same shape as the list endpoint. 404 if the reference isn't yours.
Full PAN and CVV, fetched live from the issuer. Never stored by us, and
never returned by any other endpoint. Requires dedicated cards:reveal API scope. Response is sent with Cache-Control: no-store, max-age=0, private and Pragma: no-cache.
{ "status": true, "data": { "card": {
"pan": "4821000000004821", "cvv": "123",
"expiry_month": "07", "expiry_year": "2029",
"name_on_card": "ADA OKAFOR" } } }
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.
When we call you
| Event | Sent when |
|---|---|
transaction.successful | The order was delivered. Any commission has been credited to your wallet. |
transaction.reversed | The order failed or was refunded. The amount is back in your wallet. |
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 a
pending event, because that status is already in the response to your
/purchase call.
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. |