AfroPin Developers
Postman Sign in Get test keys

Start here

Official SDKs

Libraries for PHP, Node.js, Python and Java. They sign every request, add idempotency keys, check webhook signatures and give you one error type to branch on, so you only write the calls.

Install

LanguageInstallNeeds
PHP composer require afropin/afropin-php PHP 8.0+, curl Packagist · GitHub
Node.js npm install @afropin/afropin Node 18+ npm · GitHub
Python pip install afropin Python 3.8+ PyPI · GitHub
Java com.afropin:afropin-java:1.0.0 Java 11+ Maven Central · GitHub

None of them has dependencies beyond the language itself. Each one covers every endpoint, for brands and resellers, and every reference page in these docs shows its call in all four languages under "With an SDK".

For Maven, add the dependency to your pom.xml:

<dependency>
    <groupId>com.afropin</groupId>
    <artifactId>afropin-java</artifactId>
    <version>1.0.0</version>
</dependency>

Set up a client

Create it with an API key ID and secret. Your key's mode decides whether you work with test or live records; the address is the same for both. Test and live

$afropin = new AfroPin\Client(getenv('AFROPIN_KEY_ID'), getenv('AFROPIN_SECRET'));
import { AfroPin } from '@afropin/afropin';

const afropin = new AfroPin(process.env.AFROPIN_KEY_ID, process.env.AFROPIN_SECRET);
import os
from afropin import Client

afropin = Client(os.environ["AFROPIN_KEY_ID"], os.environ["AFROPIN_SECRET"])
AfroPin afropin = new AfroPin(System.getenv("AFROPIN_KEY_ID"), System.getenv("AFROPIN_SECRET"));
Keep the secret on your server. Never put an SDK with your secret in a mobile app or browser code.

Your first call: check and redeem a PIN

Check what a customer's PIN can pay, then take the payment. Take gift cards at checkout

$check = $afropin->brand->pins->check(['pin_code' => $code, 'amount' => 2500]);
if ($check['can_redeem']) {
    $afropin->brand->pins->redeem(['pin_code' => $code, 'amount' => 2500, 'external_reference' => $saleId]);
}
const check = await afropin.brand.pins.check({ pin_code: code, amount: 2500 });
if (check.can_redeem) {
  await afropin.brand.pins.redeem({ pin_code: code, amount: 2500, external_reference: saleId });
}
check = afropin.brand.pins.check({"pin_code": code, "amount": 2500})
if check["can_redeem"]:
    afropin.brand.pins.redeem({"pin_code": code, "amount": 2500, "external_reference": sale_id})
Map<String, Object> check = afropin.brand().pins().check(Map.of("pin_code", code, "amount", 2500));
if (Boolean.TRUE.equals(check.get("can_redeem"))) {
    afropin.brand().pins().redeem(Map.of("pin_code", code, "amount", 2500, "external_reference", saleId));
}

Parameters and response fields are the API's own, exactly as the reference pages describe them. Python method names use snake_case, for example top_up; in Java, cancelling a cashout is voidCashout, as void is a keyword there.

Errors

A failed call raises one error type carrying the API's error code, its message, per-field messages and the HTTP status. Branch on the code: the message is for people and may change. Error codes

PHP
AfroPin\AfroPinException: $e->errorCode, $e->fields, $e->status
Node.js
AfroPinError: error.code, error.fields, error.status
Python
AfroPinError: error.code, error.fields, error.status
Java
AfroPinException: getCode(), getFields(), getStatus()

When the API can't be reached, the code is connection_failed and the status is 0.

Lists

List calls return a page you can loop over, with paging holding page, pages and total. Ask for the next page with page in the parameters.

const page = await afropin.brand.payments.list({ limit: 50 });
for (const item of page) { /* ... */ }
if (page.hasMore) {
  const next = await afropin.brand.payments.list({ limit: 50, page: page.paging.page + 1 });
}

Idempotency and retries

Each request is sent with a new Idempotency-Key; you can pass your own instead. The SDKs never retry for you. If a sale or top-up times out, send it again with the same external_reference: it either goes through once, or fails with duplicate_external_reference.

Webhooks

Each SDK checks the webhook-secret header against the raw body and returns the event, or raises invalid_signature. Pass the body exactly as received, before any JSON parsing. Events and webhooks

$event = AfroPin\Webhook::parse(file_get_contents('php://input'), $_SERVER['HTTP_WEBHOOK_SECRET'] ?? '', $webhookSecret);
import { parseWebhook } from '@afropin/afropin';

app.post('/afropin/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const event = parseWebhook(req.body, req.header('webhook-secret'), process.env.AFROPIN_WEBHOOK_SECRET);
  res.sendStatus(200);
});
from afropin import webhooks

event = webhooks.parse(request.get_data(), request.headers.get("webhook-secret"), WEBHOOK_SECRET)
Map<String, Object> event = Webhooks.parse(rawBody, request.getHeader("webhook-secret"), webhookSecret);

Without an SDK

Any language can call the API directly: it is HMAC-signed JSON over HTTPS. Signing requests