AfroPin Developers
Postman Sign in Get test keys

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.
The key decides the mode, not the URL: a test key puts every request in test mode. Test and live data never mix, including locations, webhooks and redemptions. Environments

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[@]}"
}
An Idempotency-Key can be used once. Sending it again is rejected with 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

  1. 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 NGN wallet, so it must appear here; if it does not, contact support before going further.

    GET /api/v1/brand/wallets

    Request

    api GET brand/wallets

    Response

    {
        "ok": true,
        "data": [
            {
                "amount": 1250,
                "currency": "NGN"
            }
        ]
    }
  2. Find the products you can redeem

    A product links a PIN template to your account. It tells you the currency, whether a PIN is whole use (always redeemed in full) or partial, whether a new PIN is issued after each debit (new_code_after_spend), and your redemption fee as a percentage, in redemption_fee_percent. Lists return 20 per page; pass limit to change that or page=all to get everything.

    GET /api/v1/brand/catalog GET /api/v1/brand/catalog/{product}

    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
        }
    }
  3. Register a redemption location

    A redemption location is a store or till where PINs are redeemed. Its id is the store_id you can attach to each redemption, so your reports show where it happened. If your account has store enforcement switched on, store_id is required on every redemption and you must do this step first. The country is an ISO 3166-1 alpha-2 code, state is the ISO code of a state in that country and postal_code must be valid for that country. Locations belong to one mode, so create one with your test key for testing.

    POST /api/v1/brand/stores GET /api/v1/brand/stores

    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 location id. An unknown, deleted or other-mode ID on a redemption fails with invalid_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 its terminal_id on 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.
  4. Redeem a test PIN

    Send the PIN the customer gave you, the amount to take from it and your own external_reference for the sale. With a test key, the PIN must be one of the test PINs; dashes are optional. Add webhook_id (one of your webhooks, same mode) to also receive a pin.spent event.

    POST /api/v1/brand/pins/redeem

    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"
        }
    }
    When new_pin_mask is not null, the PIN the customer used has been replaced: tell them their code has changed, for example on the receipt. The old code now fails with pin_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, send fee_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 carries store_id and a transaction_ref; in test mode transaction_ref is always null.
  5. Reconcile your redemptions

    Every redemption, test ones included, is recorded as a credit to your wallet. List them, newest first, optionally for one day (any date, for example 2026-10-27), and match them to your sales by external_reference. To fetch one later, use its id from this list.

    GET /api/v1/brand/payments GET /api/v1/brand/payments/{reference}

    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 redemption id is not the transaction_ref returned by the redeem call, but either one finds it with Look up a payment. The list is paged, 20 per page by default: follow paging.next or pass page, or use page=all for everything. It only shows redemptions in the mode of your key. In test mode external_reference is null in 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

  1. Complete KYC and ask for live API access to be enabled on your account.
  2. Create a live API key and swap its public key and secret key into your integration. The requests themselves do not change.
  3. Create your redemption locations and webhooks again with the live key; test ones are not visible in live mode.
  4. 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