Take gift cards at checkout
Accept AfroPin PINs as payment: check a PIN, take an amount from it and credit your wallet. This guide runs the whole flow in test mode, against test PINs that exercise every outcome. To sell your own gift cards from your systems, see the Sell your own gift cards.
Before you begin
- Your account type is brand, and PIN redemption is enabled on it. Without it every redemption fails with
redeeming_disabled. - You have a test API key: its public key and secret key, from API keys. Test keys work before KYC approval; live keys need approved KYC and live API access.
- Your account has a test NGN wallet. The test PINs are in NGN, and redeeming into a currency you have no wallet for fails with
currency_not_supported.
Sign your requests
Every request carries four signature headers and a unique Idempotency-Key. The signature is an HMAC-SHA256, keyed with your secret key, of METHOD.path.timestamp.nonce.sha256(body), where path is the URL path without the leading slash or query string. Authentication explains each part and has code in other languages.
The examples on this page use this shell helper. Paste it into your terminal with your test keys, then run each step as written.
export BASE_URL="https://afropin.com/api/v1"
export KEY_ID="YOUR_TEST_PUBLIC_KEY"
export SECRET="YOUR_TEST_SECRET_KEY"
# api METHOD ENDPOINT [JSON_BODY]: signs the request and sends it with a fresh nonce and Idempotency-Key
api() {
local method="$1" endpoint="$2" body="${3:-}"
local ts nonce url_path body_hash signature
ts=$(date +%s)
nonce=$(openssl rand -hex 16)
url_path=$(printf '%s' "$BASE_URL/$endpoint" | sed -E 's#^https?://[^/]+/##; s#\?.*$##')
body_hash=$(printf '%s' "$body" | openssl dgst -sha256 -hex | sed 's/^.* //')
signature=$(printf '%s' "$method.$url_path.$ts.$nonce.$body_hash" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
local data=()
[ -n "$body" ] && data=(--data-raw "$body")
curl -sS -X "$method" "$BASE_URL/$endpoint" \
-H "X-Key-Id: $KEY_ID" \
-H "X-Timestamp: $ts" \
-H "X-Nonce: $nonce" \
-H "X-Signature: $signature" \
-H "Idempotency-Key: $(openssl rand -hex 16)" \
-H "Content-Type: application/json" \
"${data[@]}"
}
idempotency_key_reused; the first response is not replayed. If a redemption times out, look it up in your redemptions by external_reference before retrying with a new key. Your external_reference is what stops a sale being redeemed twice: reusing it fails with duplicate_external_reference.Walk through a redemption
-
Check your balance
Start with a read-only call to prove your signing works. The response lists one wallet per currency on your account, for the mode of your key. Test redemptions credit your test
GET /api/v1/brand/walletsNGNwallet, so it must appear here; if it does not, contact support before going further.Request
api GET brand/walletsResponse
{ "ok": true, "data": [ { "amount": 1250, "currency": "NGN" } ] } -
Find the products you can redeem
A product links a PIN template to your account. It tells you the currency, whether a PIN is
GET /api/v1/brand/catalog GET /api/v1/brand/catalog/{product}wholeuse (always redeemed in full) orpartial, whether a new PIN is issued after each debit (new_code_after_spend), and your redemption fee as a percentage, inredemption_fee_percent. Lists return 20 per page; passlimitto change that orpage=allto get everything.Request
api GET "brand/catalog?limit=20"Response (key fields)
{ "ok": true, "data": [ { "id": "6a0f3c2e-8d41-4b7a-9e15-2c7d9b0a4f63", "pin_type_id": "c2b9e1d4-5f6a-47b8-9c0d-1e2f3a4b5c6d", "name": "Everyday Shopping PIN", "currency": "NGN", "min": 5, "max": 500, "amount_rule": "any_amount", "can_spend": true, "spend_rule": "partial", "new_code_after_spend": true, "redemption_fee_percent": 1.5 } ], "paging": { "page": 1, "pages": 1, "per_page": 20, "total": 1, "next": null, "previous": null } } -
Register a redemption location
A redemption location is a store or till where PINs are redeemed. Its
POST /api/v1/brand/stores GET /api/v1/brand/storesidis thestore_idyou can attach to each redemption, so your reports show where it happened. If your account has store enforcement switched on,store_idis required on every redemption and you must do this step first. Thecountryis an ISO 3166-1 alpha-2 code,stateis the ISO code of a state in that country andpostal_codemust be valid for that country. Locations belong to one mode, so create one with your test key for testing.Request
api POST brand/stores '{ "title": "Lekki Phase 1 Store", "address": "12 Admiralty Way", "latitude": 6.4474, "longitude": 3.4723, "city": "Lekki", "state": "LA", "postal_code": "106104", "country": "NG" }'Response
{ "ok": true, "data": { "id": "8e2d4f6a-1b3c-4d5e-9f70-a1b2c3d4e5f6", "title": "Lekki Phase 1 Store", "latitude": "6.4474", "longitude": "3.4723", "address": "12 Admiralty Way", "country": "NG", "city": "Lekki", "state": "LA", "postal_code": "106104" } }Store the locationid. An unknown, deleted or other-mode ID on a redemption fails withinvalid_store_id. A store with several tills can add a terminal for each one on the store's Terminals page in your dashboard, then send itsterminal_idon Redeem to record which till ran each redemption. Staff can also sign in to the terminal portal with the terminal's login ID and password to redeem PINs without any integration. -
Redeem a test PIN
Send the PIN the customer gave you, the amount to take from it and your own
POST /api/v1/brand/pins/redeemexternal_referencefor the sale. With a test key, the PIN must be one of the test PINs; dashes are optional. Addwebhook_id(one of your webhooks, same mode) to also receive apin.spentevent.Request
api POST brand/pins/redeem '{ "pin_code": "2340-1000-0000-1001", "amount": 2500, "external_reference": "order-1001", "store_id": "8e2d4f6a-1b3c-4d5e-9f70-a1b2c3d4e5f6" }'Response
{ "ok": true, "data": { "transaction_ref": null, "new_pin_mask": "7676-****-****-6608", "amount": 2500, "balance": 22500, "currency": "NGN", "mode": "test", "external_reference": "order-1001", "processed_at": "2026-10-27T09:14:22.000000Z" } }Whennew_pin_maskis notnull, the PIN the customer used has been replaced: tell them their code has changed, for example on the receipt. The old code now fails withpin_code_replaced. Brands are never sent the full new code; the customer gets it from the business they bought the PIN from. To pass your redemption fee to the customer, sendfee_paid_by: customer: the PIN then pays the amount plus the fee and your wallet gets the full amount. In live mode the response also carriesstore_idand atransaction_ref; in test modetransaction_refis alwaysnull. -
Reconcile your redemptions
Every redemption, test ones included, is recorded as a credit to your wallet. List them, newest first, optionally for one
GET /api/v1/brand/payments GET /api/v1/brand/payments/{reference}day(any date, for example2026-10-27), and match them to your sales byexternal_reference. To fetch one later, use itsidfrom this list.Request
api GET "brand/payments?day=2026-10-27&limit=20"Response
{ "ok": true, "data": [ { "id": "f1e2d3c4-b5a6-4978-8a6b-5c4d3e2f1a0b", "external_reference": null, "store_id": "8e2d4f6a-1b3c-4d5e-9f70-a1b2c3d4e5f6", "pin_mask": "2340-****-****-1001", "amount": 2500, "currency": "NGN", "balance_before": 125000, "balance_after": 127500, "direction": "in", "type": "pin_spend", "status": "success", "mode": "test", "created_at": "2026-10-27T09:14:22.000000Z" } ], "paging": { "page": 1, "pages": 1, "per_page": 20, "total": 1, "next": null, "previous": null } }The redemptionidis not thetransaction_refreturned by the redeem call, but either one finds it with Look up a payment. The list is paged, 20 per page by default: followpaging.nextor passpage, or usepage=allfor everything. It only shows redemptions in the mode of your key. In test modeexternal_referenceisnullin this list; it is filled in for live redemptions.
Test PINs
With a test key, redemptions run against these fixed PINs, all in NGN. Each drives one outcome. They never change: every redemption starts from the balance below, so you can repeat a test as often as you like, and the balance in the response is that balance minus your amount.
| PIN | Balance | Status | Type | New PIN | Use it to test |
|---|---|---|---|---|---|
2340-1000-0000-1001 |
25000 NGN | active | Partial | Yes | A successful partial redemption that returns a new PIN. |
2340-1000-0000-2002 |
0 NGN | active | Partial | Yes | An empty PIN: pin_balance_low. |
2340-1000-0000-3003 |
7500 NGN | expired | Partial | Yes | An expired PIN that still holds money: pin_expired. |
2340-1000-0000-4004 |
12000 NGN | disabled | Partial | Yes | A blocked PIN: pin_blocked. |
2340-1000-0000-5005 |
15000 NGN | active | Single use | Yes | Single use: the amount must be exactly 15,000, and a new PIN is returned. Any other amount fails with full_balance_required. |
2340-1000-0000-6006 |
5000 NGN | active | Single use | No | Single use: the amount must be exactly 5,000; no new PIN is returned. |
Handle failures
Failed requests return "ok": false and an error object with a stable code; branch on the code, not the translated message. Validation errors also list each field in error.fields. Use the test PINs to drive each of these on purpose.
| Code | HTTP | When |
|---|---|---|
validation_error |
422 | A required field is missing or the wrong type, or store_id is missing while store enforcement is on. |
wrong_account_type |
403 | The key belongs to an account that is not a brand. |
redeeming_disabled |
403 | PIN redemption is switched off for your account. Contact support. |
invalid_store_id |
422 | No redemption location with that ID in this mode. |
test_pin_unknown |
404 | Test mode only: the PIN is not one of the test PINs. |
pin_checksum_failed |
422 | Live mode only: the PIN fails its check digit, usually a typo. |
pin_code_unknown |
404 | Live mode only: no PIN matches. |
pin_code_replaced |
403 | Live mode only: an older PIN was used after a new one was issued. |
pin_not_accepted_here |
403 | Live mode only: you have no product for this PIN's template. |
product_inactive |
403 | Live mode only: your product for this PIN has been switched off. |
pin_type_inactive |
403 | Live mode only: the PIN's template has been switched off. |
pin_blocked |
403 | The reseller disabled the PIN. |
pin_expired |
403 | The PIN has expired. |
pin_balance_low |
402 | The amount is more than the PIN holds. |
full_balance_required |
403 | Single-use PIN and the amount is not its full balance. The message states the balance. |
duplicate_external_reference |
422 | You already used this external_reference. |
invalid_webhook_id |
404 | No webhook with that ID in this mode. |
currency_not_supported |
403 | You have no wallet in the PIN's currency. |
rate_limited |
429 | Another request from your account is still being processed. Retry after a moment. |
Signature and key failures (invalid_api_key, invalid_signature, missing_auth_headers, timestamp_out_of_window, nonce_replayed) and Idempotency-Key failures apply to every endpoint. All error codes →
Test mode and live mode
| Test mode | Live mode | |
|---|---|---|
| Access | Any test key, before KYC. | Approved KYC and live API access. |
| PINs | The fixed test PINs above; their balances never change. | Real PINs issued by resellers, for templates you have a product for. |
| Check digit | Not checked. | Checked first: a mistyped PIN fails with pin_checksum_failed. |
| Your wallet | Credited with the full amount. | Credited with the amount less your redemption fee. |
transaction_ref |
null |
The redemption's transaction reference. |
| Webhooks | Sent to the webhook_id you pass. |
Sent to the webhook_id you pass, and to the reseller's webhook for the PIN. |
Go live
- Complete KYC and ask for live API access to be enabled on your account.
- Create a live API key and swap its public key and secret key into your integration. The requests themselves do not change.
- Create your redemption locations and webhooks again with the live key; test ones are not visible in live mode.
- Make sure you have a live wallet in each currency of the products you redeem, and allow for your redemption fee when reconciling.
Next steps
- Take payment with a PIN for every field and error of the redeem call.
- Sell your own gift cards to sell your own gift cards through the API.
- Purchase locations to show customers where they can buy PINs you accept.
- Webhooks to receive
pin.spentevents and verify their signature. - Go-live checklist to check your integration before you go live.