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.
<!-- 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.
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 -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
{
"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.
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 });
}
);
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 successfullytest_failure— payment fails at the providertest_pending— payment stays in pending statetest_reversal— payment completes then reverses
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.
@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
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.
@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.
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.