AFA Portal AfaPortal ← Back to sign in
Public API v1

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

  1. Sign in to your AFA Portal account and open API Keys.
  2. Generate a key and copy it somewhere safe — it's shown only once.
  3. Send it as an X-API-KEY header 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_idYesAn id from the packages endpoint
phoneYesRecipient's Ghana number — 0201234567 or 233201234567
client_referenceRecommendedYour 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_urlNoHTTPS 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.