📦 Merchant SDK

Embed AfriLink Pay in Minutes

Accept mobile money, bank transfers and payment links with 3 lines of code. Drop our JavaScript widget into any page, or call the API directly from Python, Node.js, or any HTTP client.

Quick start

Choose your integration method. The JavaScript drop-in is the fastest way to accept payments on a web page. The Python and cURL examples show how to initiate payments from your server.

HTML + JavaScript
<!-- 1. Load the AfriLink Pay SDK -->
<script src="https://afrilinkpay.com/sdk/afrilink.js"></script>

<!-- 2. Place the checkout div wherever you want the button -->
<div id="afrilink-checkout"
     data-merchant-key="mk_live_xxxxxxxxxxxxxxxx"
     data-amount="1000"
     data-currency="GHS"
     data-reference="order-123"
     data-on-success="onPaymentSuccess"
     data-on-failure="onPaymentFailure">
</div>

<!-- 3. Handle the result -->
<script>
  function onPaymentSuccess(txn) {
    console.log('Paid:', txn.paymentId);
    // txn = { paymentId, status, amount, currency, reference }
  }
  function onPaymentFailure(err) {
    console.error('Failed:', err.message);
  }
</script>

The SDK auto-detects the customer's country, presents available payment rails (mobile money, bank transfer, QR), and handles the entire checkout flow inside a modal. No redirect needed.

Python
import requests

response = requests.post(
    'https://afrilinkpay.com/api/pay',
    headers={
        'x-merchant-api-key': 'mk_live_xxx',
        'Content-Type': 'application/json'
    },
    json={
        "amount":    1000,
        "currency":  "GHS",
        "rail":      "mobile_money",
        "receiver":  {"phone": "+233501234567"},
        "reference": "order-123"
    }
)

data = response.json()
print(data)
# {
#   "paymentId": "afl_pay_abc123",
#   "status":    "pending",
#   "fxRate":    1.0,
#   "fee":       8.5,
#   "estimatedArrival": "2026-05-20T14:30:00Z"
# }
cURL
curl -X POST https://afrilinkpay.com/api/pay \
  -H "x-merchant-api-key: mk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "amount":   1000,
    "currency": "GHS",
    "rail":     "mobile_money",
    "receiver": {"phone": "+233501234567"},
    "reference": "order-123"
  }'

Authentication

Every API request must include your merchant API key in the x-merchant-api-key header. Get your key in the merchant dashboard under Settings → Integrations.

🔑 Live keys vs. test keys

Keys starting with mk_live_ process real payments. Keys starting with mk_test_ run in sandbox mode — no real money moves, and the API returns simulated responses identical to production.

x-merchant-api-key: mk_live_a1b2c3...  // production
x-merchant-api-key: mk_test_a1b2c3... // sandbox

🛡️ Keep your key secret

Never expose your API key in client-side JavaScript or commit it to version control. The drop-in widget uses a publishable key (a separate restricted key) to initiate the modal; your full merchant key stays on your server.


Webhook setup

Configure a webhook URL in your merchant dashboard under Settings → Webhooks. AfriLink Pay will POST signed JSON events to your endpoint for every payment state transition.

Sample event payload

JSON
{
  "event":     "payment.completed",
  "paymentId": "afl_pay_abc123",
  "status":    "completed",
  "amount":    1000,
  "currency":  "GHS",
  "destinationAmount":   51230,
  "destinationCurrency": "NGN",
  "fxRate":    51.23,
  "fee":       8.50,
  "reference": "order-123",
  "timestamp": "2026-05-20T14:30:00Z"
}

Signature verification — x-afrilink-signature

Every webhook request includes an x-afrilink-signature header — an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. Always verify before processing.

Node.js — 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 sig      = req.headers['x-afrilink-signature'];
    const expected = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(req.body)          // raw Buffer
      .digest('hex');

    const valid = sig?.length === expected.length &&
      crypto.timingSafeEqual(
        Buffer.from(sig,      'utf8'),
        Buffer.from(expected, 'utf8')
      );

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

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

    if (event.event === 'payment.completed') {
      fulfillOrder(event.reference, event.paymentId);
    }

    res.status(200).json({ received: true });
  }
);
💡
AfriLink retries failed webhook deliveries up to 5 times with exponential back-off. Your endpoint must respond with HTTP 2xx within 10 seconds. The same paymentId is sent on every retry — use it to deduplicate.

Test mode

Use a mk_test_ prefix API key to simulate the full payment lifecycle in the sandbox. No real money moves, and no KYC checks are enforced. Sandbox payments progress through all status transitions (pending → processing → completed) automatically within a few seconds.

🧪 Simulate outcomes

Set reference to a special test value to force specific outcomes:

  • test_success — payment completes successfully
  • test_failure — payment fails at the provider
  • test_pending — payment stays in pending state
  • test_reversal — payment completes then reverses

🔔 Test webhooks

Sandbox payments trigger real webhook calls to your configured endpoint. Use a tunnelling service like ngrok or smee.io to receive them on your local machine during development.


Supported payment rails

The rail field in the payment request selects which payment method to use. Availability varies by country.

Rail Description Countries Notes
mobile_money Send directly to a mobile wallet (MTN MoMo, M-Pesa, Airtel, etc.) NG, KE, GH, TZ, UG, RW, CM, SN + 30 more Live
bank_transfer Transfer to a local bank account via RTGS / NEFT / mobile banking NG, KE, GH, ZA, EG, SN + 20 more Live
payment_link Generate a shareable link; customer chooses their preferred method All 54 countries Live
qr QR code displayed at point of sale; customer scans with mobile wallet NG, KE, GH, ZA, TZ Live
offline_qr Static QR for environments with intermittent connectivity; syncs when online NG, KE, GH, TZ, UG Beta
card Card-on-file and card tokenisation via Paystack / Flutterwave NG, GH, ZA, EG, KE Beta
cash_pickup Recipient collects cash at a registered agent location NG, KE, GH, UG, TZ, SN, CI Beta

Live = available now   Beta = available by invitation   Planned = on the roadmap


POS Terminal API

Turn any NFC-capable Android phone into a merchant terminal. Your customer taps their AfriLink wearable, the wallet debit and merchant credit settle in real time — no card networks, no hardware lease, no gateway fees.

📟 The POS Terminal is closed-loop: funds move between AfriLink wallets only. Works with AfriLink wristbands, tags, and any NFC device loaded with an AfriLink NDEF URL. No-NFC fallback via QR scan or manual @handle entry.
// 1. Merchant opens a POS session (amount in smallest currency unit)
const res = await fetch('/api/pos/sessions', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer <merchant_api_key>'
  },
  body: JSON.stringify({
    amount:      5000,        // e.g. 5000 = KES 50.00
    currency:    'KES',
    description: 'Coffee x2'
  })
});
const { sessionId, qrPayload, expiresAt } = await res.json();
// sessionId — poll or webhook target
// qrPayload — show as QR for non-NFC fallback
🔗 See the full flow, hardware API reference, and comparison table on the POS Terminal page →

Gift Cards API

Issue cryptographic gift cards backed by real wallet balance, then let anyone redeem them into their own AfriLink wallet. Codes are 256-bit and stored only as a SHA-256 hash — the full code is returned exactly once at creation. Endpoints exist for individual wallet sessions, merchants, agents, and admins; the examples below use the merchant surface.

🎁 Issuing debits the issuer's wallet atomically; redeeming credits the redeemer's wallet. Cross-type redemption is allowed (an agent can redeem a card issued by an individual). Optional PIN and @handle lock add recipient controls.
// 1. Issue a gift card (debits the merchant wallet)
const res = await fetch('/api/merchant/gift-cards', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-merchant-api-key': '<merchant_api_key>'
  },
  body: JSON.stringify({
    amount:          50,            // in the card currency
    currency:        'GHS',        // USD, GHS, NGN, KES, ZAR, EUR, GBP
    designTheme:     'birthday',   // default | birthday | business | celebration
    message:         'Happy birthday!',  // optional, ≤160 chars
    pinCode:         '4821',        // optional, 4–8 digits
    lockedToHandle:  'amara',       // optional — only this @handle may redeem
    expiresInMonths: 12,            // 1–24, default 12
    brandConfig: {                  // optional (Phase 2) — branded card
      brandName:    'Acme Rewards',
      logoUrl:      'https://cdn.acme.com/logo.png',  // https only
      primaryColor: '#00d9a3', accentColor: '#ffb800'  // hex
    }
  })
});
const card = await res.json();
// { id, code, qrPng, codePrefix, amount, currency, expiresAt, designTheme }
// ⚠️ `code` is plaintext and returned ONCE — show/share it immediately.
🔒 Limits: per-card value is capped by KYC tier (Tier 0 → $50, 1 → $500, 2 → $5,000, 3 → unlimited) and you may issue up to 10 cards per wallet per 24 h (429 when exceeded). Individual @handle sessions use the same shapes at /api/wallet/gift-cards* with a Authorization: Bearer wallet.<token> header; agents use /api/agent/gift-cards* with their session token.

Ready to embed AfriLink Pay?

Get your merchant API key, drop in 3 lines of code, and start accepting payments across Africa today.