Integrate AFA Registrations & Data Bundles
Submit AFA registrations and sell MTN, Telecel and AirtelTigo data bundles directly from your own website, app, or backend using a personal API key.
Getting Started
- Sign in to your AFA Portal account and open API Keys.
- Generate a key and copy it somewhere safe — it's shown only once.
- Send it as an
X-API-KEYheader on every request below.
Authentication
Every request must include your API key in an X-API-KEY header.
Requests are billed to the wallet of the account that owns the key — AFA registrations at that account's tier fee and data bundles at the listed package price, exactly like using the portal.
X-API-KEY: YOUR_API_KEY
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/afa/fee | Get your current registration fee and tier |
| POST | /api/v1/afa/register | Submit a new AFA registration |
| GET | /api/v1/afa/registrations | Paginated list of your submitted registrations |
| GET | /api/v1/data/packages | Data bundles you can buy right now, with prices |
| POST | /api/v1/data/purchase | Buy a data bundle for any Ghana number |
| GET | /api/v1/data/orders | Paginated list of your data orders |
| GET | /api/v1/data/orders/{reference} | One data order by our reference or your client_reference |
Create a Registration
POST /api/v1/afa/register — debits your wallet at your tier's fee and creates a pending registration for admin review.
curl -X POST https://afaportal.com/api/v1/afa/register \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"full_name": "Kwame Mensah",
"phone": "0241234567",
"id_card_number": "GHA-123456789-0",
"location": "Accra, Madina"
}'<?php
$apiKey = 'YOUR_API_KEY';
$ch = curl_init('https://afaportal.com/api/v1/afa/register');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-API-KEY: ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'full_name' => 'Kwame Mensah',
'phone' => '0241234567',
'id_card_number' => 'GHA-123456789-0',
'location' => 'Accra, Madina',
]),
]);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($statusCode === 201) {
echo "Registration submitted: #{$data['data']['id']}";
} else {
echo "Error: {$data['message']}";
}const response = await fetch('https://afaportal.com/api/v1/afa/register', {
method: 'POST',
headers: {
'X-API-KEY': 'YOUR_API_KEY',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
full_name: 'Kwame Mensah',
phone: '0241234567',
id_card_number: 'GHA-123456789-0',
location: 'Accra, Madina',
}),
});
const data = await response.json();
if (response.ok) {
console.log('Registration submitted:', data.data.id);
} else {
console.error('Error:', data.message);
}// Node.js 18+ (built-in fetch, no extra dependencies)
async function submitAfaRegistration() {
const response = await fetch('https://afaportal.com/api/v1/afa/register', {
method: 'POST',
headers: {
'X-API-KEY': process.env.AFA_API_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
full_name: 'Kwame Mensah',
phone: '0241234567',
id_card_number: 'GHA-123456789-0',
location: 'Accra, Madina',
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'AFA registration failed');
}
return data.data;
}
submitAfaRegistration()
.then((registration) => console.log('Submitted:', registration.id))
.catch((err) => console.error(err.message));import requests
API_KEY = 'YOUR_API_KEY'
BASE_URL = 'https://afaportal.com'
response = requests.post(
f'{BASE_URL}/api/v1/afa/register',
headers={
'X-API-KEY': API_KEY,
'Accept': 'application/json',
},
json={
'full_name': 'Kwame Mensah',
'phone': '0241234567',
'id_card_number': 'GHA-123456789-0',
'location': 'Accra, Madina',
},
)
data = response.json()
if response.status_code == 201:
print(f"Registration submitted: #{data['data']['id']}")
else:
print(f"Error: {data['message']}")Example response — 201 Created
{
"data": {
"id": 123,
"status": "pending",
"status_label": "Pending",
"fee_paid": "15.00",
"full_name": "Kwame Mensah",
"phone": "0241234567",
"id_card_number": "GHA-123456789-0",
"location": "Accra, Madina",
"submitter_tier": "agent",
"admin_notes": null,
"created_at": "2026-09-11T09:00:00+00:00",
"updated_at": "2026-09-11T09:00:00+00:00"
},
"message": "Registration submitted. GHS 15.00 was deducted from your wallet.",
"reference": "AFA-42-123"
}
Fee & History
Get your current fee before submitting:
curl https://afaportal.com/api/v1/afa/fee \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Accept: application/json"
List your submitted registrations, paginated:
curl "https://afaportal.com/api/v1/afa/registrations?page=1&per_page=15" \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Accept: application/json"
Data: Packages
GET /api/v1/data/packages — every bundle you can buy right now, with the price your wallet will be charged.
Filter with ?network=mtn, ?network=telecel or ?network=airteltigo.
Availability and prices can change, so fetch this list regularly rather than hard-coding package IDs or prices.
curl "https://afaportal.com/api/v1/data/packages?network=telecel" \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Accept: application/json"
Example response — 200 OK
{
"data": [
{
"id": 24,
"network": "telecel",
"network_label": "Telecel",
"name": "10GB",
"capacity_mb": 10000,
"price": "36.00"
},
{
"id": 25,
"network": "telecel",
"network_label": "Telecel",
"name": "12GB",
"capacity_mb": 12000,
"price": "43.00"
}
]
}
Data: Buy a Bundle
POST /api/v1/data/purchase — debits your wallet at the package price and sends the bundle for delivery straight away.
| Field | Required | Description |
|---|---|---|
| package_id | Yes | An id from the packages endpoint |
| phone | Yes | Recipient's Ghana number — 0201234567 or 233201234567 |
| client_reference | Recommended | Your own order ID (max 64 chars), unique per account. Sending the same value again never charges twice — you get the original order back with a 409, so it is safe to retry after a timeout. |
| callback_url | No | HTTPS URL we POST to when the order is delivered or fails — see Callbacks |
curl -X POST https://afaportal.com/api/v1/data/purchase \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"package_id": 24,
"phone": "0201234567",
"client_reference": "ORDER-10045",
"callback_url": "https://your-site.com/webhooks/data"
}'<?php
$apiKey = 'YOUR_API_KEY';
$ch = curl_init('https://afaportal.com/api/v1/data/purchase');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-API-KEY: ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'package_id' => 24,
'phone' => '0201234567',
'client_reference' => 'ORDER-10045', // your own order ID — safe to retry
'callback_url' => 'https://your-site.com/webhooks/data',
]),
]);
$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($statusCode === 201) {
echo "Order {$data['data']['reference']} is {$data['data']['status']}";
} else {
echo "Error ({$statusCode}): {$data['message']}";
}// Call this from your server — never expose your API key in browser code.
const response = await fetch('https://afaportal.com/api/v1/data/purchase', {
method: 'POST',
headers: {
'X-API-KEY': 'YOUR_API_KEY',
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
package_id: 24,
phone: '0201234567',
client_reference: 'ORDER-10045',
callback_url: 'https://your-site.com/webhooks/data',
}),
});
const data = await response.json();
if (response.status === 201) {
console.log('Order placed:', data.data.reference, data.data.status);
} else {
console.error('Error:', data.message);
}// Node.js 18+ (built-in fetch, no extra dependencies)
async function buyData(packageId, phone, orderId) {
const response = await fetch('https://afaportal.com/api/v1/data/purchase', {
method: 'POST',
headers: {
'X-API-KEY': process.env.AFA_API_KEY,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
package_id: packageId,
phone,
client_reference: orderId,
callback_url: 'https://your-site.com/webhooks/data',
}),
});
const data = await response.json();
if (response.status === 409) {
return data.data; // already placed earlier with this client_reference
}
if (!response.ok) {
throw new Error(data.message || 'Data purchase failed');
}
return data.data;
}
buyData(24, '0201234567', 'ORDER-10045')
.then((order) => console.log(order.reference, order.status))
.catch((err) => console.error(err.message));import requests
API_KEY = 'YOUR_API_KEY'
BASE_URL = 'https://afaportal.com'
response = requests.post(
f'{BASE_URL}/api/v1/data/purchase',
headers={
'X-API-KEY': API_KEY,
'Accept': 'application/json',
},
json={
'package_id': 24,
'phone': '0201234567',
'client_reference': 'ORDER-10045',
'callback_url': 'https://your-site.com/webhooks/data',
},
)
data = response.json()
if response.status_code == 201:
print(f"Order {data['data']['reference']} is {data['data']['status']}")
else:
print(f"Error ({response.status_code}): {data['message']}")Example response — 201 Created
{
"data": {
"reference": "DT-K2M9QX7ZP4LA",
"client_reference": "ORDER-10045",
"status": "processing",
"status_label": "Processing",
"network": "telecel",
"package": "10GB",
"package_id": 24,
"capacity_mb": 10000,
"recipient_phone": "0201234567",
"amount": "36.00",
"refunded": false,
"failure_reason": null,
"delivered_at": null,
"created_at": "2026-10-11T09:00:00+00:00",
"updated_at": "2026-10-11T09:00:01+00:00"
},
"message": "Order received. GHS 36.00 was deducted from your wallet."
}
The status of an order is one of:
| pending | Paid and queued — we are still handing it to the network. Retried automatically. |
| processing | Accepted by the network and on its way to the recipient |
| completed | Delivered to the recipient |
| failed | Could not be delivered — the amount is refunded to your wallet automatically (refunded: true) and failure_reason explains why |
Data: Order Status
Look up one order by the reference we returned or by your own client_reference, or page through your history.
If you are not using callbacks, poll an order no more than once a minute until it is completed or failed.
# By our reference or by your client_reference
curl https://afaportal.com/api/v1/data/orders/ORDER-10045 \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Accept: application/json"
# Your order history (optional ?status=pending|processing|completed|failed)
curl "https://afaportal.com/api/v1/data/orders?page=1&per_page=15" \
-H "X-API-KEY: YOUR_API_KEY" \
-H "Accept: application/json"
Data: Callbacks
If you send a callback_url, we POST the final order to it once, when the order becomes
completed (data.order.delivered) or
failed (data.order.failed). Respond with any 2xx status.
POST https://your-site.com/webhooks/data
Content-Type: application/json
{
"event": "data.order.delivered",
"data": {
"reference": "DT-K2M9QX7ZP4LA",
"client_reference": "ORDER-10045",
"status": "completed",
"status_label": "Delivered",
"network": "telecel",
"package": "10GB",
"recipient_phone": "0201234567",
"amount": "36.00",
"refunded": false,
"delivered_at": "2026-10-11T09:02:13+00:00",
...
}
}
Callbacks are not signed and are not retried. Treat one as a prompt to re-fetch the order from
GET /api/v1/data/orders/{reference} with your API key before you act on it, and keep polling as a fallback.
Errors
| Status | Meaning |
|---|---|
| 401 | Missing, invalid, or revoked API key |
| 400 | Validation failed — check the errors object in the response |
| 402 | Insufficient wallet balance — top up before retrying |
| 404 | Data order not found on your account |
| 409 | AFA: an active registration already exists for that phone or ID number. Data: an order with that client_reference already exists — the original order is returned in data and you were not charged again |
| 422 | Data: the network refused the bundle — the order is returned with status: failed and the amount was refunded to your wallet |
| 503 | Data: that package is not available right now — refresh the packages list and try another |
| 429 | Rate limit exceeded — see below |
Rate Limits
Each API key is limited to 60 requests per minute. Exceeding this returns a 429 response.