AfroPin Developers
Postman Sign in Get test keys

Taking payment

Take payment with a PIN

Take payment from a customer's PIN. The amount is debited from the PIN and credited to your wallet in the PIN's currency.

POST https://afropin.com/api/v1/brand/pins/redeem

HMAC-signed request with a unique Idempotency-Key header. Authentication

With an SDK
PHP$afropin->brand->pins->redeem($params)
Node.jsafropin.brand.pins.redeem(params)
Pythonafropin.brand.pins.redeem(params)
Javaafropin.brand().pins().redeem(params)
Not sure a PIN will go through? Call Check a PIN first: it returns the balance and whether you can redeem it, without debiting anything.

Body parameters

pin_code string Required

The PIN code the customer presents, with or without dashes, for example 3093-0622-2855-3097. It must be the latest code issued for the PIN; an older code fails with pin_code_replaced.

amount numeric Required

Amount to redeem, in the PIN's currency. For a partial product it can be any amount up to the PIN balance. For a whole product it must equal the full remaining balance.

external_reference string Required max 191

Your own reference for this redemption, unique across your account. Reusing one fails with duplicate_external_reference, so a retry with the same reference can never charge the PIN twice.

store_id uuid Required if your account enforces store IDs max 255

ID of the store taking the payment, from List redemption locations. Optional unless support has turned on store enforcement for your account; a terminal_id satisfies that too. When sent, it must be one of your stores in the same mode.

terminal_id uuid Optional max 255

ID of the POS terminal (till) that ran the redemption, from the store's Terminals page in your dashboard. It is recorded on the redemption and returned in the response, so each branch can see which till redeemed what. A terminal belongs to one store: its store is used as store_id, and a different store_id fails with terminal_store_mismatch. An unknown or deleted terminal fails with invalid_terminal_id, a disabled one with terminal_disabled.

fee_paid_by string Optional brand customer

Who pays the redemption fee (the redemption_fee_percent percentage on the product). Defaults to brand: the PIN pays amount and your wallet gets it less the fee. customer: the fee is added on top, so the PIN pays amount plus the fee and your wallet gets the full amount. The PIN must cover both. A single-use PIN still gives up its whole balance, so with customer the amount must be its balance less the fee: take it from Check a PIN (redeemable_amount with fee_paid_by=customer). Test PINs carry no fee.

webhook_id uuid Optional

ID of one of your webhooks in the same mode. A pin.spent event is sent to it once the redemption succeeds. Webhooks

Response fields

transaction_ref uuid | null Required

ID of the debit on the PIN. Store it with your order. Always null in test mode.

store_id uuid | null Live mode only

The store you sent, or the terminal's store when you sent only terminal_id. Not included in test responses.

terminal_id uuid | null Required

The terminal you sent. null when none was sent.

new_pin_mask string | null Required

When the product issues a new code on every redemption, the mask of the PIN's new code, for example 8710-****-****-7718. Tell the customer their code has changed: the one they presented no longer works. The full new code is never sent to brands; the customer gets it from the business they bought the PIN from. null otherwise.

amount number Required

The amount redeemed, as sent.

fee number Required

The redemption fee on this redemption. 0 in test mode.

fee_paid_by string Required brand customer

Who paid the fee, as sent (default brand).

amount_debited number Required

What left the PIN: amount, plus fee when the customer paid it.

amount_credited number Required

What your wallet received: amount less fee when you paid it, the full amount when the customer did.

balance number Required

PIN balance left after this redemption.

currency string Required

Currency of the PIN and of the wallet credited.

mode string Required test live

Mode of the API key used.

external_reference string Required

Your reference, as sent.

processed_at string Required

When the redemption was processed, ISO 8601 in UTC.

Brands never receive PIN codes
A redemption that changes the PIN's code returns only the new code's mask, so nobody at the till can reuse the customer's PIN after they leave. Tell the customer their code has changed; the new code reaches them from the business they bought the PIN from.
In live mode your wallet is credited the amount less your redemption fee, the redemption_fee_percent percentage on the product. The PIN itself is debited the full amount; for a whole product the whole balance is used and the PIN is left at zero.
Only one write request per account is processed at a time. A redemption sent while another request from your account is still running fails with rate_limited; wait a moment and retry with the same external_reference and a new Idempotency-Key.

Test PIN codes

Test keys never touch real PINs: only these codes are accepted, and the Luhn check is skipped. Each simulates a different state. Your test wallet in NGN is credited the full amount.

CodeBalanceBehaviour
234010000000100125,000 NGNActive, partial redemption, new code after each redemption.
23401000000020020 NGNActive but empty: fails with pin_balance_low.
23401000000030037,500 NGNExpired, with money still on it: fails with pin_expired.
234010000000400412,000 NGNBlocked: fails with pin_blocked.
234010000000500515,000 NGNSingle use: the amount must be exactly 15,000 NGN, otherwise full_balance_required. New code after redemption.
23401000000060065,000 NGNSingle use, amount must be exactly 5,000 NGN. No new code: new_pin_mask is null.

Test balances are fixed, so the same code can be redeemed again; send a new external_reference each time. Any other code fails with test_pin_unknown.

Request

{
    "pin_code": "3093-0622-2855-3097",
    "amount": 2500,
    "external_reference": "order_98231",
    "store_id": "5c1e8f2a-7b3d-4e19-a6c0-2d9f4b8e1a73",
    "terminal_id": "9b7d2c41-3e8f-4a65-b1d0-7c2e9f4a8b16",
    "webhook_id": "3f6c1b8e-9a2d-4c7e-8b1f-5d0a9e4c2b71",
    "fee_paid_by": "customer"
}

Response 200

{
    "ok": true,
    "data": {
        "transaction_ref": "dcb2e747-5988-4553-ad05-371b5e707724",
        "store_id": "5c1e8f2a-7b3d-4e19-a6c0-2d9f4b8e1a73",
        "terminal_id": "9b7d2c41-3e8f-4a65-b1d0-7c2e9f4a8b16",
        "new_pin_mask": "8710-****-****-7718",
        "amount": 2500,
        "fee": 150,
        "fee_paid_by": "customer",
        "amount_debited": 2650,
        "amount_credited": 2500,
        "balance": 7350,
        "currency": "NGN",
        "mode": "live",
        "external_reference": "order_98231",
        "processed_at": "2026-10-27T09:14:22.000000Z"
    }
}

Errors

Failed requests return a stable code with a translated message. Branch on code, never on the message.

422 validation_error Some fields are missing or invalid
403 wrong_account_type Your account type cannot use this endpoint
403 redeeming_disabled Redeeming PINs is not switched on for your account. Contact support
429 rate_limited Too many requests. Wait a moment and try again
422 invalid_store_id No store of yours has this store_id
422 invalid_terminal_id No terminal of yours has this terminal_id
403 terminal_disabled This terminal is switched off
422 terminal_store_mismatch This terminal_id belongs to a different store than store_id
422 pin_checksum_failed This is not a valid PIN code: its check digit is wrong
404 pin_code_unknown No PIN has this code
403 pin_code_replaced This code has been replaced by a newer one; ask the customer for their latest code
403 pin_not_accepted_here Your account does not accept this PIN type
403 product_inactive This product is switched off
403 pin_type_inactive This PIN type is paused
404 test_pin_unknown Test keys only accept the test PINs listed in the docs
403 pin_blocked This PIN is blocked
403 pin_expired This PIN has expired
402 pin_balance_low The PIN's balance does not cover this amount
403 full_balance_required This PIN is single use: the amount must be its whole balance
403 pin_daily_limit_reached This PIN has a daily spending limit, and this amount is over what is left of it today
422 duplicate_external_reference You have sent this external_reference before
404 invalid_webhook_id No webhook of yours has this webhook_id
403 currency_not_supported Your account has no wallet in this currency
500 pin_generation_failed A new PIN code could not be created. Try again
500 internal_error Something went wrong on our side. Try again; if it keeps happening, contact support
{
    "ok": false,
    "error": {
        "code": "wrong_account_type",
        "message": "Your account type cannot use this endpoint",
        "fields": null
    }
}
All error codes →