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.
HMAC-signed request with a unique Idempotency-Key header. Authentication
$afropin->brand->pins->redeem($params)afropin.brand.pins.redeem(params)afropin.brand.pins.redeem(params)afropin.brand().pins().redeem(params)Body parameters
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 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.
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.
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.
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.
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.
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
ID of the debit on the PIN. Store it with your order. Always null in test mode.
The store you sent, or the terminal's store when you sent only terminal_id. Not included in test responses.
The terminal you sent. null when none was sent.
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.
The amount redeemed, as sent.
The redemption fee on this redemption. 0 in test mode.
Who paid the fee, as sent (default brand).
What left the PIN: amount, plus fee when the customer paid it.
What your wallet received: amount less fee when you paid it, the full amount when the customer did.
PIN balance left after this redemption.
Currency of the PIN and of the wallet credited.
Mode of the API key used.
Your reference, as sent.
When the redemption was processed, ISO 8601 in UTC.
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.
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.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.
| Code | Balance | Behaviour |
|---|---|---|
2340100000001001 | 25,000 NGN | Active, partial redemption, new code after each redemption. |
2340100000002002 | 0 NGN | Active but empty: fails with pin_balance_low. |
2340100000003003 | 7,500 NGN | Expired, with money still on it: fails with pin_expired. |
2340100000004004 | 12,000 NGN | Blocked: fails with pin_blocked. |
2340100000005005 | 15,000 NGN | Single use: the amount must be exactly 15,000 NGN, otherwise full_balance_required. New code after redemption. |
2340100000006006 | 5,000 NGN | Single 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.
validation_error
Some fields are missing or invalid
wrong_account_type
Your account type cannot use this endpoint
redeeming_disabled
Redeeming PINs is not switched on for your account. Contact support
rate_limited
Too many requests. Wait a moment and try again
invalid_store_id
No store of yours has this store_id
invalid_terminal_id
No terminal of yours has this terminal_id
terminal_disabled
This terminal is switched off
terminal_store_mismatch
This terminal_id belongs to a different store than store_id
pin_checksum_failed
This is not a valid PIN code: its check digit is wrong
pin_code_unknown
No PIN has this code
pin_code_replaced
This code has been replaced by a newer one; ask the customer for their latest code
pin_not_accepted_here
Your account does not accept this PIN type
product_inactive
This product is switched off
pin_type_inactive
This PIN type is paused
test_pin_unknown
Test keys only accept the test PINs listed in the docs
pin_blocked
This PIN is blocked
pin_expired
This PIN has expired
pin_balance_low
The PIN's balance does not cover this amount
full_balance_required
This PIN is single use: the amount must be its whole balance
pin_daily_limit_reached
This PIN has a daily spending limit, and this amount is over what is left of it today
duplicate_external_reference
You have sent this external_reference before
invalid_webhook_id
No webhook of yours has this webhook_id
currency_not_supported
Your account has no wallet in this currency
pin_generation_failed
A new PIN code could not be created. Try again
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 →