⚡ Build on AfriLink

Build on AfriLink Pay

Powerful REST APIs for cross-border payments, FX quotes, bill payments, and payment identity across all 54 African countries. Simple. Reliable. Made for Africa.

Quick start

Make your first cross-border payment with a single API call. Replace YOUR_API_KEY with your merchant API key and set the corridor details.

JavaScript
// POST /api/pay — initiate a cross-border transfer
const response = await fetch('https://afrilinkpay.com/api/pay', {
  method: 'POST',
  headers: {
    'Content-Type':       'application/json',
    'x-merchant-api-key': 'YOUR_API_KEY',
    'Idempotency-Key':    crypto.randomUUID(), // prevents duplicate charges
  },
  body: JSON.stringify({
    fromCode:     'GH',            // ISO-3166-1 alpha-2 sender country
    toCode:       'NG',            // ISO-3166-1 alpha-2 recipient country
    rail:         'mobile_money',  // 'mobile_money' | 'bank_transfer' | 'wallet' | 'cash_pickup'
    amount:       500,             // amount in sender's currency
    currency:     'GHS',          // sender's currency (ISO 4217)
    receiver:     '+2348012345678',// recipient phone or account number
    receiverName: 'Chidinma Okafor',
    senderName:   'Kwame Asante',
    note:         'Family support – May 2026',
  }),
});

const { success, transactionId, status, destinationAmount, destinationCurrency } = await response.json();
// success: true
// transactionId: 'afl_pay_abc123'
// status: 'pending'
// destinationAmount: 59000, destinationCurrency: 'NGN'

Poll for status

JavaScript
// GET /api/payments/:transactionId — check payment status (public, no auth needed)
const res    = await fetch('https://afrilinkpay.com/api/payments/afl_pay_abc123');
const { id, status, amount, currency, destinationAmount, destinationCurrency, createdAt } = await res.json();
// status: 'pending' | 'completed' | 'failed'

Authentication

All API requests must include your merchant API key as the x-merchant-api-key header. Keys are generated in the merchant dashboard under Settings → API Keys.

🔑 API key format

Keys are 64-character hexadecimal strings generated with cryptographic entropy. They cannot be recovered if lost — generate a new one and rotate the old key.

x-merchant-api-key: afl_live_a1b2c3d4e5f6...

🔄 Key rotation

Rotate API keys immediately if you suspect a compromise. The merchant dashboard lets you generate a new key and set a grace period (up to 24 hours) during which both old and new keys are accepted.

⚠️
Never expose your API key in client-side JavaScript or commit it to version control. Use environment variables or a secrets manager. Requests must originate from your server, not a browser.

Key endpoints

Endpoint Method Description
/api/pay POST Initiate a cross-border or domestic transfer. Requires Idempotency-Key header. Returns paymentId, fxRate, fee, and estimatedArrival.
/api/payments/:id GET Retrieve a payment by its paymentId. Returns current status, timeline, provider reference, and any failure reason.
/api/pay-identity/:handle GET Resolve an AfriLink Pay Identity handle to the linked payment destinations (mobile wallets, bank accounts, preferred rail).
/api/fx/quote GET Get a live FX quote for a corridor. Parameters: from (currency), to (currency), amount. Returns mid-market rate, your spread rate, estimated fee, and quote expiry time.
/api/bills/pay POST Pay a utility bill, mobile airtime top-up, or cable TV subscription. Supports NG, GH, and KE billers. Body: billType, billerId, accountRef, amount.
/api/payment-links POST Create a shareable payment link for a fixed or variable amount. The link can be single-use or reusable and expires on a date you set.
/api/fx/rates GET Retrieve the current FX rate table for all supported currency pairs. Rates are refreshed every 5 minutes from multiple sources.
/api/tap/:tagId GET Resolve an AfriLink Tap NFC tag to its linked @handle and payment redirect URL. Public, no auth required. Increments the tag's tap counter.
/api/wearables POST Register a new NFC wearable tag linked to a Pay Identity @handle. Returns a unique tagId (e.g. TG3F8A1B2C) and tapUrl to write to the physical tag.
/api/pos/sessions POST Create a POS payment session. Body: amount, currency, description, optional callbackUrl. Returns sessionId, tapUrl, and ttlSeconds: 90. Merchant API key required.
/api/pos/sessions/:id/tap POST Submit a wearable tap to a session. Body: tagId (e.g. TG3F8A1B2C). Resolves tag → @handle → wallet debit/credit atomically. Returns completed session with payerHandle and walletRef.
/api/pos/sessions/:id GET Poll a session for status changes. Status machine: pending → tapped → processing → completed | failed | expired | cancelled. Use 1-second polling or webhooks.
/api/merchant/gift-cards POST Issue a gift card backed by wallet balance. Body: amount, currency, optional message, pinCode, lockedToHandle, designTheme, expiresInMonths, brandConfig ({logoUrl(https), primaryColor(#hex), accentColor, brandName}), for scheduled delivery deliverToHandle + deliverAt (future ISO datetime — auto-redeemed into the recipient's wallet then), and for subscription gifting subscriptionPlanId + giftMonths (merchant-only; amount auto-priced at plan × months; redeeming activates the plan for the recipient instead of crediting a wallet). Returns { id, code, qrPng, codePrefix, expiresAt, amountRemaining, brand, deliveryScheduledAt, subscriptionPlanId, giftMonths }code is plaintext and returned ONCE. (Also /api/wallet/gift-cards and /api/agent/gift-cards.)
/api/merchant/gift-cards/bulk POST Bulk-issue up to 100 identical cards. Body: count, amount, currency, optional designTheme, expiresInMonths. Returns { batchId, cards }. (Bulk/import bypass the per-issuer 10/24h single-issue rate limit.)
/api/merchant/gift-cards/import POST CSV import — issue up to 500 different cards. Body: { cards: [{ amount, currency, message?, designTheme?, expiresInMonths?, lockedToHandle? }] }. Per-row failures are collected, not fatal. Returns { batchId, issuedCount, failedCount, issued, failed }.
/api/gift-cards/verify GET Public — verify a code via ?code=AFL-… without revealing the issuer. Returns { valid, amount, currency, expiresAt, hasPin, locked }. No auth required.
/api/merchant/gift-cards/redeem POST Redeem a card into the caller's wallet. Body: code, optional pinCode, optional redeemAmount (partial — card stays active with a remaining balance; omit for full), optional targetCurrency (credit in another currency at live FX). 409 already used, 410 expired, 422 amount > remaining, 403 wrong PIN / handle lock. (Also /api/wallet/… and /api/agent/….)
/api/{admin,merchant}/gift-cards/:id/redemptions GET Per-card redemption history (the partial / multi-merchant spend trail): each row has amount_drawn, credited_amount/currency, fx_rate, redeemer, timestamp. Merchant scope is limited to cards they issued. (Issue with multiMerchant:true to mark a "spend at any AfriLink merchant" card.)
/api/admin/gift-cards GET Admin — list/filter all cards (status, issuerType, date range, pagination); /stats for aggregates; PATCH /api/admin/gift-cards/:id with { action: 'cancel' | 'refund' } to force-cancel or refund to the issuer.
/api/merchant/gift-card-programs POST B2B programs — create ({name, description}), GET to list, GET /:id for detail + program-scoped analytics, PATCH /:id to rename/archive. POST /:id/issue with { employees: [{handle, amount, currency, deliverAt?, externalRef?}] } issues a card per employee (locked to their @handle, optionally auto-delivered) under the program. Corporate/HR API: pass a per-row externalRef to make retries idempotent — already-issued refs are returned in skipped instead of re-issued. Response: { issuedCount, failedCount, skippedCount, issued, failed, skipped }.
/api/merchant/reward-rules POST Loyalty engine — define a rule ({name, minSpend, percent, maxReward?, currency}); GET to list, PATCH /:id to update/archive. POST /api/merchant/rewards/evaluate with {customerHandle, spendAmount, currency} picks the best matching active rule and issues a cashback gift card locked to the customer — call it from your checkout on qualifying spend.
/api/merchant/gift-cards/analytics GET Redemption analytics for your issued cards: issuedCount/Value, redeemedCount, partialCount, activeCount, outstandingValue, totalDrawn, redemptionRatePct, avgMinutesToFirstRedeem, and a byCurrency breakdown. (Also /api/agent/…; admin: /api/admin/gift-cards/analytics?issuerType=&issuerId= for scoped or platform-wide.)
/api/health GET Platform health check. Returns { ok: true, uptime, dbLatency }. No auth required. Use this in your integration monitoring.

SDKs & Drop-in Widget

Choose your integration path — from a one-line drop-in to full REST API access in any language.

🌐

Browser Drop-in

One script tag. Opens a polished payment modal with automatic currency conversion and rail selection.

<script src="/sdk/afrilink.js"></script>
Live now
📦

Node.js SDK

Server-side client with TypeScript types, auto-retry, idempotency key management, and webhook verification helpers.

npm install afrilink-pay
Planned Q3 2026
🐍

Python SDK

Django, Flask, and FastAPI support with async httpx client. Includes Celery-friendly webhook handlers.

Planned Q4 2026
Full SDK reference & interactive examples →
Code tabs for every SDK, runnable sample apps, and the browser drop-in quick-start guide.
Open SDK docs →

Webhooks

AfriLink Pay sends signed webhook events to your endpoint for all payment state transitions. Configure your webhook URL in the merchant dashboard under Settings → Webhooks.

Payload structure

JSON
{
  "event":     "payment.completed",
  "paymentId": "afl_pay_abc123",
  "status":    "completed",
  "amount":    500,
  "currency":  "GHS",
  "destinationAmount":   24500,
  "destinationCurrency": "NGN",
  "fxRate":    51.23,
  "timestamp": "2026-05-20T14:30:00Z",
  "reference": "order_2026_001"
}

Signature verification (Node.js)

Every webhook includes an x-afrilink-signature header. Verify it before processing to ensure the event came from AfriLink and hasn't been tampered with.

JavaScript — Express.js
import crypto from 'crypto';

const WEBHOOK_SECRET = process.env.AFRILINK_WEBHOOK_SECRET;

app.post('/webhooks/afrilink', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-afrilink-signature'];
  const body      = req.body; // raw Buffer — DO NOT use express.json() here

  // Compute HMAC-SHA256 of the raw request body
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(body)
    .digest('hex');

  // Constant-time comparison to prevent timing attacks
  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature ?? '', 'utf8'),
    Buffer.from(expected,        'utf8'),
  );

  if (!isValid) return res.status(401).json({ error: 'Invalid signature' });

  const event = JSON.parse(body.toString('utf8'));

  switch (event.event) {
    case 'payment.completed':
      await fulfillOrder(event.reference, event.paymentId);
      break;
    case 'payment.failed':
      await notifyCustomerOfFailure(event.reference, event.paymentId);
      break;
    case 'payment.reversed':
      await refundOrder(event.reference);
      break;
  }

  res.status(200).json({ received: true });
});
💡
AfriLink Pay retries failed webhook deliveries up to 5 times with exponential backoff (1 min, 5 min, 30 min, 2 h, 24 h). Your endpoint must return HTTP 2xx within 10 seconds. Return the same 2xx on retry — our payloads include the same paymentId so you can detect and skip duplicates.

What you can build

AfriLink Pay's API powers payment flows across all 54 African countries. Here are six production-proven use cases to get you started.

🛒

E-commerce Checkout

Embed a pay-by-phone checkout on your website or app. Customers enter their mobile money number, choose a wallet (M-Pesa, MTN MoMo, Airtel…), and pay in their local currency. You receive settlement in your preferred currency.

Key endpoints: /api/pay, /api/fx/quote, webhooks

📱

Airtime & Data Top-Up

Let users top up any phone on any network in Nigeria, Ghana, or Kenya. One API call: specify billType: "airtime", the phone number, and the amount. Works with Airtel, MTN, Glo, and 9mobile.

Key endpoints: /api/bills/pay

💼

Payroll Disbursement

Pay staff across multiple countries in a single batch. Submit payroll runs via /api/payroll, and AfriLink routes each payment to the right mobile money wallet or bank account — regardless of which country the employee is in.

Key endpoints: /api/payroll/runs, /api/payroll/items

🌍

Remittance Widget

Embed a white-label "Send Money" widget on your diaspora platform. Show a live FX quote, let the sender pick a rail, and trigger the transfer — all with three API calls. Our corridor-first routing finds the best path automatically.

Key endpoints: /api/fx/quote, /api/pay, /api/payments/:id

📷

QR Payment Terminal

Generate a merchant QR code that customers scan to pay. Works offline — transactions queue on the customer's device and settle automatically when connectivity returns. Ideal for market traders, event vendors, and kiosks.

Key endpoints: /api/payment-links, /api/offline/qr

🔁

Subscription Billing

Create subscription plans and charge recurring payments from mobile money wallets. Define billing cycles, trial periods, and dunning logic. Our webhook events notify you of successful charges, failed attempts, and cancellations.

Key endpoints: /api/subscriptions, webhooks

📟

NFC POS Terminal

Turn any NFC Android phone into a merchant payment terminal. Create a POS session, customers tap their AfriLink wearable, and funds settle in the merchant's wallet in under 3 seconds. Hardware API for dedicated POS devices included.

Key endpoints: /api/pos/sessions, /api/pos/sessions/:id/tap, webhooks

POS Terminal docs →
🎁

Gift Cards & Rewards

Issue cryptographic gift cards backed by wallet balance — one at a time or up to 100 in a single bulk call for promotions and payroll rewards. Recipients redeem the code or QR into any AfriLink wallet. Optional PIN and @handle lock add recipient controls; codes are stored only as a hash and can be redeemed exactly once.

Key endpoints: /api/merchant/gift-cards, /api/merchant/gift-cards/bulk, /api/gift-cards/verify

Gift Cards docs →

Copy-paste sample apps

Each snippet below is a complete, self-contained HTML file you can open in a browser and test against the live sandbox. Replace YOUR_API_KEY with your merchant API key.

Sample App 1 — Send Money form

A minimal payment form that initiates a cross-border mobile money transfer and shows the response in real time.

HTML — save as pay-demo.html and open in a browser
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>AfriLink Pay — Send Money Demo</title>
  <style>
    body { font-family: sans-serif; max-width: 480px; margin: 2rem auto; padding: 0 1rem; }
    input, select { display: block; width: 100%; padding: .6rem; margin: .4rem 0 1rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; }
    button { background: #FFB800; color: #000; border: none; padding: .75rem 2rem; font-size: 1rem; border-radius: 6px; cursor: pointer; }
    pre { background: #f4f4f4; padding: 1rem; border-radius: 6px; white-space: pre-wrap; word-break: break-all; }
  </style>
</head>
<body>
  <h1>AfriLink Pay — Send Money</h1>

  <label>From country code</label>
  <input id="fromCode" value="GH" maxlength="2" />

  <label>To country code</label>
  <input id="toCode" value="NG" maxlength="2" />

  <label>Amount (in sender's currency)</label>
  <input id="amount" type="number" value="500" />

  <label>Sender currency (ISO 4217)</label>
  <input id="currency" value="GHS" maxlength="3" />

  <label>Recipient phone (E.164 format)</label>
  <input id="phone" value="+2348012345678" />

  <label>Rail</label>
  <select id="rail">
    <option value="mobile_money">Mobile Money</option>
    <option value="bank_transfer">Bank Transfer</option>
  </select>

  <button onclick="sendPayment()">Send Payment →</button>
  <pre id="result" style="margin-top:1.5rem"></pre>

  <script>
    const API_KEY = 'YOUR_API_KEY';           // ← replace this
    const BASE    = 'https://afrilinkpay.com';

    async function sendPayment() {
      const body = {
        fromCode:     document.getElementById('fromCode').value.toUpperCase(),
        toCode:       document.getElementById('toCode').value.toUpperCase(),
        rail:         document.getElementById('rail').value,
        amount:       Number(document.getElementById('amount').value),
        currency:     document.getElementById('currency').value.toUpperCase(),
        receiver:     document.getElementById('phone').value,
        receiverName: 'Demo Recipient',
        senderName:   'API Demo',
      };
      document.getElementById('result').textContent = 'Sending…';
      try {
        const res = await fetch(BASE + '/api/pay', {
          method: 'POST',
          headers: {
            'Content-Type':       'application/json',
            'x-merchant-api-key': API_KEY,
            'Idempotency-Key':    crypto.randomUUID(),
          },
          body: JSON.stringify(body),
        });
        const data = await res.json();
        document.getElementById('result').textContent = JSON.stringify(data, null, 2);
      } catch (err) {
        document.getElementById('result').textContent = 'Error: ' + err.message;
      }
    }
  </script>
</body>
</html>

Sample App 2 — Live FX quote widget

A currency converter that shows live exchange rates and the recipient amount before a payment is initiated. No API key needed — uses the public GET /api/fx endpoint.

HTML — save as fx-widget.html and open in a browser
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>AfriLink Pay — FX Quote</title>
  <style>
    body { font-family: sans-serif; max-width: 420px; margin: 2rem auto; padding: 0 1rem; }
    select, input { padding: .6rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; }
    .row { display: flex; gap: .5rem; align-items: center; margin-bottom: 1rem; }
    .row input { flex: 1; }
    .result { background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1rem; margin-top: 1.5rem; }
    .rate   { font-size: 1.4rem; font-weight: 700; color: #0a7c5c; }
  </style>
</head>
<body>
  <h1>AfriLink Pay — FX Quote</h1>
  <p style="font-size:.85rem;color:#888">Select two country codes and an amount to see the live exchange rate.</p>

  <div class="row">
    <input id="amount" type="number" value="100" oninput="getQuote()" />
    <select id="from" onchange="getQuote()">
      <option value="GH">GH (Ghana · GHS)</option>
      <option value="NG">NG (Nigeria · NGN)</option>
      <option value="KE">KE (Kenya · KES)</option>
      <option value="ZA">ZA (South Africa · ZAR)</option>
      <option value="US">US (United States · USD)</option>
      <option value="GB">GB (United Kingdom · GBP)</option>
    </select>
  </div>

  <p style="text-align:center;color:#888;margin:-.5rem 0 .75rem">→ to →</p>

  <select id="to" onchange="getQuote()" style="width:100%;margin-bottom:1rem">
    <option value="NG">NG (Nigeria · NGN)</option>
    <option value="GH">GH (Ghana · GHS)</option>
    <option value="KE">KE (Kenya · KES)</option>
    <option value="ZA">ZA (South Africa · ZAR)</option>
    <option value="US">US (United States · USD)</option>
    <option value="GB">GB (United Kingdom · GBP)</option>
  </select>

  <div class="result" id="result">Loading…</div>

  <script>
    /* Endpoint: GET /api/fx?from={countryCode}&to={countryCode}&amount={n}
       Returns: { fromCurrency, toCurrency, rate, amount, receive } */
    const BASE = 'https://afrilinkpay.com';
    let timer;

    async function getQuote() {
      clearTimeout(timer);
      timer = setTimeout(async () => {
        const from   = document.getElementById('from').value;
        const to     = document.getElementById('to').value;
        const amount = Number(document.getElementById('amount').value) || 100;
        if (from === to) { document.getElementById('result').innerHTML = 'Choose different countries.'; return; }
        document.getElementById('result').innerHTML = 'Fetching quote…';
        try {
          const r = await fetch(`${BASE}/api/fx?from=${from}&to=${to}&amount=${amount}`);
          const q = await r.json();
          if (!r.ok) { document.getElementById('result').innerHTML = 'Error: ' + (q.error || r.status); return; }
          const fromCcy = q.fromCurrency || from;
          const toCcy   = q.toCurrency   || to;
          const rate    = q.rate    != null ? Number(q.rate).toFixed(4)    : '—';
          const receive = q.receive != null ? Number(q.receive).toFixed(2)  : '—';
          document.getElementById('result').innerHTML = `
            <p>Rate: <span class="rate">1 ${fromCcy} = ${rate} ${toCcy}</span></p>
            <p>Sending: <strong>${amount} ${fromCcy}</strong></p>
            <p>Recipient receives: <strong>${receive} ${toCcy}</strong></p>
            <p style="font-size:.8rem;color:#888;margin-top:.5rem">Rates updated every 5 min. Final rate locked at payment time.</p>
          `;
        } catch (err) {
          document.getElementById('result').innerHTML = 'Could not fetch rates — check your connection.';
        }
      }, 400);
    }
    getQuote();
  </script>
</body>
</html>

Sample App 3 — Payment status tracker

Enter a paymentId to see the current status, FX details, and timeline. Useful for building order-tracking pages or debugging integrations.

HTML — save as status-tracker.html and open in a browser
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>AfriLink Pay — Payment Status Tracker</title>
  <style>
    body  { font-family: sans-serif; max-width: 560px; margin: 2rem auto; padding: 0 1rem; }
    input { display: block; width: 100%; padding: .6rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; margin-bottom: 1rem; box-sizing: border-box; }
    button { background: #FFB800; color: #000; border: none; padding: .75rem 2rem; font-size: 1rem; border-radius: 6px; cursor: pointer; }
    pre    { background: #f4f4f4; padding: 1rem; border-radius: 6px; white-space: pre-wrap; word-break: break-all; font-size: .9rem; }
    .status-badge { display: inline-block; padding: .25em .75em; border-radius: 999px; font-weight: 700; font-size: .85rem; }
    .status-completed  { background: #d4edda; color: #155724; }
    .status-pending    { background: #fff3cd; color: #856404; }
    .status-processing { background: #cce5ff; color: #004085; }
    .status-failed     { background: #f8d7da; color: #721c24; }
    .detail-row { display: flex; justify-content: space-between; padding: .5rem 0; border-bottom: 1px solid #eee; font-size: .95rem; }
  </style>
</head>
<body>
  <h1>Payment Status Tracker</h1>

  <label>Payment ID (the transactionId returned by /api/pay)</label>
  <input id="payId" placeholder="paste transactionId here…" />

  <button onclick="checkStatus()">Check Status</button>

  <div id="result" style="margin-top:1.5rem"></div>

  <script>
    const BASE = 'https://afrilinkpay.com';

    async function checkStatus() {
      const payId  = document.getElementById('payId').value.trim();
      if (!payId) { alert('Enter a payment ID.'); return; }

      document.getElementById('result').innerHTML = 'Fetching…';
      try {
        const r = await fetch(`${BASE}/api/payments/${encodeURIComponent(payId)}`);
        const d = await r.json();
        if (!r.ok) { document.getElementById('result').innerHTML = `<pre>${JSON.stringify(d, null, 2)}</pre>`; return; }

        const status    = d.status || '—';
        const badgeCls  = 'status-' + status;
        const arrived   = d.estimatedArrival ? new Date(d.estimatedArrival).toLocaleString() : '—';
        const updated   = d.updatedAt        ? new Date(d.updatedAt).toLocaleString()        : '—';

        document.getElementById('result').innerHTML = `
          <h3>Payment <code>${d.id || d.transactionId || payId}</code></h3>
          <p>Status: <span class="status-badge ${badgeCls}">${status}</span></p>
          <div class="detail-row"><span>Amount sent</span><strong>${d.amount ?? '—'} ${d.currency ?? ''}</strong></div>
          <div class="detail-row"><span>Recipient receives</span><strong>${d.destinationAmount ?? '—'} ${d.destinationCurrency ?? ''}</strong></div>
          <div class="detail-row"><span>FX rate</span><strong>${d.fxRate ?? '—'}</strong></div>
          <div class="detail-row"><span>Fee</span><strong>${d.fee ?? '—'}</strong></div>
          <div class="detail-row"><span>Est. arrival</span><strong>${arrived}</strong></div>
          <div class="detail-row"><span>Last updated</span><strong>${updated}</strong></div>
          ${d.failureReason ? `<div class="detail-row"><span>Failure reason</span><strong style="color:#721c24">${d.failureReason}</strong></div>` : ''}
          <details style="margin-top:1rem"><summary style="cursor:pointer;color:#888">Full JSON response</summary><pre>${JSON.stringify(d, null, 2)}</pre></details>
        `;
      } catch (err) {
        document.getElementById('result').innerHTML = `<p style="color:red">Error: ${err.message}</p>`;
      }
    }
  </script>
</body>
</html>
💡
These demos point to the live AfriLink Pay sandbox at afrilinkpay.com. All transactions in the sandbox are simulated — no real money moves. Request a sandbox API key at dev@afrilinkpay.com.

Rate limits

Endpoint groupLimitWindowResponse when exceeded
/api/pay 120 requests 60 seconds per API key HTTP 429 with Retry-After header
/api/payments/* 300 requests 60 seconds per API key HTTP 429
/api/fx/* 600 requests 60 seconds per IP HTTP 429
/api/bills/* 60 requests 60 seconds per API key HTTP 429
All other /api/* endpoints 120 requests 60 seconds per API key HTTP 429

Rate limit headers are included on every response: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. If you need higher limits for production workloads, contact dev@afrilinkpay.com.


📱 AfriLink Tap — NFC Wearables API

Transform any NFC sticker, ring, band, card, or keychain into a contactless payment point. Link it to an AfriLink @handle — one tap opens the payment page in any browser, with no app required.

📲

Register a tag

Call POST /api/wearables with your merchant API key to register any NFC tag type (sticker, card, ring, band, keychain, watch, custom) and link it to a Pay Identity @handle.

🔓

Public resolve endpoint

GET /api/tap/:tagId needs no auth — tag IDs are public by design (like short URLs). The endpoint returns the handle, display name, and redirect URL, and increments the tap counter.

Integration in 2 API calls

JavaScript
// Step 1 — Register tag (server-side, merchant API key)
const tag = await fetch('https://afrilinkpay.com/api/wearables', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-merchant-api-key': 'YOUR_KEY' },
  body: JSON.stringify({ handle: '@myshop', tagType: 'ring', displayName: 'Checkout Ring' }),
}).then(r => r.json());

// tag.tapUrl = 'https://afrilinkpay.com/tap/TG3F8A1B2C'
// → Write tag.tapUrl to the physical NFC tag (Web NFC or any NFC app)

// Step 2 — Resolve a tap (client-side, no auth)
const resolved = await fetch('https://afrilinkpay.com/api/tap/TG3F8A1B2C').then(r => r.json());
// resolved.redirectTo = '/pay/@myshop?source=nfc'
window.location.href = resolved.redirectTo; // send user to payment page
🔄
Update without re-programming. Change which @handle a tag resolves to anytime via PUT /api/wearables/:id — the physical tag URL never changes. Supports all major NFC tag types (NTAG213/215/216, DESFire, MIFARE Ultralight, etc.) via standard NDEF URI records. Full AfriLink Tap documentation →

POS Terminal API

Build hardware or software POS terminals that accept AfriLink wearable taps. Sessions expire after 90 seconds and support polling and HMAC-signed webhooks.

Full session lifecycle

JavaScript — hardware POS device integration
// Step 1 — POS device creates a session
const session = await fetch('https://afrilinkpay.com/api/pos/sessions', {
  method:  'POST',
  headers: { 'Content-Type': 'application/json', 'x-merchant-api-key': 'YOUR_KEY' },
  body: JSON.stringify({
    amount:      25.00,
    currency:    'GHS',
    description: 'Groceries',
    callbackUrl: 'https://your-device.local/pos-webhook',  // optional — HMAC-signed
  }),
}).then(r => r.json());

// session.sessionId   → UUID for this payment session
// session.tapUrl      → https://afrilinkpay.com/tap/TGxxxxxxxx  (for QR display)
// session.ttlSeconds  → 90   (session expires in 90 seconds)
// session.status      → 'pending'

// Step 2 — Customer taps wearable. Your device reads the NDEF URL via Web NFC:
const reader = new NDEFReader();
await reader.scan();
reader.addEventListener('reading', async ({ message }) => {
  for (const record of message.records) {
    if (record.recordType === 'url') {
      const url   = new TextDecoder().decode(record.data);
      const match = url.match(/\/tap\/(TG[0-9A-F]{8})/i);
      if (match) {
        // Step 3 — Submit the tap to the session
        await fetch(`https://afrilinkpay.com/api/pos/sessions/${session.sessionId}/tap`, {
          method:  'POST',
          headers: { 'Content-Type': 'application/json', 'x-merchant-api-key': 'YOUR_KEY' },
          body:    JSON.stringify({ tagId: match[1] }),
        });
      }
    }
  }
});

// Step 4 — Poll for completion (or use callbackUrl webhook)
const poll = setInterval(async () => {
  const s = await fetch(`https://afrilinkpay.com/api/pos/sessions/${session.sessionId}`, {
    headers: { 'x-merchant-api-key': 'YOUR_KEY' },
  }).then(r => r.json());

  if (s.status === 'completed') {
    clearInterval(poll);
    console.log('Paid by', s.payerHandle, '— wallet ref:', s.walletRef);
    // → show green success screen on your POS device
  }
  if (['failed','expired','cancelled'].includes(s.status)) {
    clearInterval(poll);
    console.error('Session ended:', s.status, s.errorMessage);
  }
}, 1000);

POS webhook payload

If you provided a callbackUrl when creating the session, AfriLink will POST a signed event there on completion or failure. Verify the X-AfriLink-Signature header before processing.

JSON — pos.session.completed event
{
  "event":       "pos.session.completed",
  "sessionId":   "b3a2c1d0-...",
  "merchantId":  "merchant_uuid",
  "amount":      25.00,
  "currency":    "GHS",
  "payerHandle": "@kwame",
  "walletRef":   "wlt_txn_abc123",
  "completedAt": "2026-05-26T13:00:00Z"
}

// Header: X-AfriLink-Signature: sha256=
// Verify: HMAC-SHA256(rawBody, WEBHOOK_SECRET) === signature
📱
Using the built-in web POS? Navigate to /pos on any NFC-capable Android phone (Chrome 89+). No hardware integration needed — the web POS handles NFC reading, session polling, and fallbacks automatically. Merchants and agents access it directly from their dashboards. POS Terminal docs →

Ready to build?

Get your API key, explore the full documentation, and join 500+ developers building on AfriLink Pay.