Build your integration
Sell your own gift cards
Sell your own gift cards from your website, app or systems: the same calls resellers use, at your /brand/ paths. This guide sells a gift card in test mode and follows it through its life. To accept gift cards at your checkout, see the Take gift cards at checkout.
Before you begin
- Your account officer has set up at least one of your products for selling, and selling is switched on for your account. Without it, selling calls fail with
catalog_item_not_foundorissuing_disabled. - You have a test API key from API keys. The same key works for redeeming and selling.
- Your test issuing balance holds funds in the product's currency.
Sign your requests
Requests are signed exactly as for redemption; see Authentication. The examples below 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[@]}"
}
external_reference is what stops a sale being made twice: reusing it fails with duplicate_external_reference. If a sale times out, look for your reference before retrying with a new Idempotency-Key.Sell and manage a gift card
-
Check your issuing balance
Start with a read-only call to prove your signing works. Each wallet's
GET /api/v1/brand/walletsamountis your issuing balance in that currency: selling or topping up one of your gift cards draws its face value from it. Fund it from your dashboard, or move money across from your redemption balance. If your test wallet is empty, contact support to fund it.Request
api GET brand/walletsResponse
{ "ok": true, "data": [ { "amount": 250000, "currency": "NGN" } ] } -
Pick a product you sell
These are your own gift cards that our team has set up for you to sell. Note the
GET /api/v1/brand/issuing-catalogid: you pass it ascatalog_idwhen selling. It also gives the amounts you can sell (any amount frommintomaxforany_amount, or one of thedenominationsforset_amounts), how many days a gift card lasts (duration) and whether it can be topped up.sale_commission_percentis always 0 on your own products: you sell at face value.Request
api GET "brand/issuing-catalog?limit=20"Response (key fields)
{ "ok": true, "data": [ { "id": "3d7e9a1b-2c4f-4e6a-8b0d-9f1e2a3b4c5d", "pin_type_id": "c2b9e1d4-5f6a-47b8-9c0d-1e2f3a4b5c6d", "name": "Lekki Bistro Gift Card", "currency": "NGN", "min": 5000, "max": 50000, "amount_rule": "set_amounts", "denominations": [5000, 10000, 25000, 50000], "duration": 365, "can_top_up": false, "spend_rule": "partial", "new_code_after_top_up": false, "new_code_after_spend": true, "sale_commission_percent": 0 } ], "paging": { "page": 1, "pages": 1, "per_page": 20, "total": 1, "next": null, "previous": null } }A product you only accept, and don't sell, is not listed here. Ask your account officer to set one up for selling. -
Sell a gift card
Sell a gift card to your customer. Its face value (
POST /api/v1/brand/pinsamount) is taken from your issuing balance; there is no commission. The customer is emailed their gift card with a QR code for your till. Passstore_id(one of your stores, from Your stores) so your reports show where it was sold, andwebhook_idto follow it with events. Add"wallet_links": trueto get Apple Wallet and Google Wallet links in the response.Request
api POST brand/pins '{ "catalog_id": "3d7e9a1b-2c4f-4e6a-8b0d-9f1e2a3b4c5d", "amount": 10000, "external_reference": "order-5001", "first_name": "Jane", "last_name": "Doe", "email": "[email protected]", "store_id": "5b7c9d1e-3f2a-4b6c-8d0e-1f2a3b4c5d6e", "webhook_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d" }'Response
{ "ok": true, "data": { "pin_id": "9f1c2e7a-4b22-4c10-9a3e-7d2f1b6c8a90", "webhook_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "store_id": "5b7c9d1e-3f2a-4b6c-8d0e-1f2a3b4c5d6e", "transaction_ref": "3c6bb90c-4dc9-4e01-bdf2-75e91199847d", "pin_code": "5260-1815-9083-0164", "pin_mask": "5260-****-****-0164", "amount": 10000, "balance": 10000, "currency": "NGN", "mode": "test", "customer": { "first_name": "Jane", "last_name": "Doe", "email": "[email protected]", "phone": null }, "status": "active", "external_reference": "order-5001", "expires_at": "2027-11-04T10:02:11.000000Z", "sold_at": "2026-11-04T10:02:11.000000Z" } }Storepin_id: every later call about this gift card uses it. If your own app shows the code to the customer, savepin_codesecurely first: we never store codes, so this is the only response that contains it in full. Storing PIN codes. -
Look up the gift card
Fetch its current balance and status (
GET /api/v1/brand/pins/{voucher}active,blockedorexpired). When the customer spends it at your stores, the balance here goes down.Request
api GET brand/pins/9f1c2e7a-4b22-4c10-9a3e-7d2f1b6c8a90Response
{ "ok": true, "data": { "pin_id": "9f1c2e7a-4b22-4c10-9a3e-7d2f1b6c8a90", "balance": 10000, "currency": "NGN", "pin_mask": "5260-****-****-0164", "past_pin_masks": [ "5260-****-****-0164" ], "mode": "test", "status": "active", "expires_at": "2027-11-04 10:02:11", "sold_at": "2026-11-04T10:02:11.000000Z" } } -
Top it up, if the product allows
On a product with
POST /api/v1/brand/pins/top-upcan_top_up: true, add value to a gift card the customer already has. The new balance must stay within the product'sminandmax, and the amount is taken from your issuing balance.Request
api POST brand/pins/top-up '{ "pin_id": "9f1c2e7a-4b22-4c10-9a3e-7d2f1b6c8a90", "amount": 5000, "external_reference": "order-5002" }'Response
{ "ok": true, "data": { "pin_id": "9f1c2e7a-4b22-4c10-9a3e-7d2f1b6c8a90", "transaction_ref": "a41d7c2e-9b3f-4e8a-b6d1-0c5f2e7a9b31", "new_pin_code": null, "new_pin_mask": null, "amount": 5000, "balance": 15000, "currency": "NGN", "mode": "test", "status": "active", "external_reference": "order-5002" } }If the product gives a new code after each top-up,new_pin_codeholds it, returned only this once; the customer is emailed it too. -
Follow its history
Every movement of value on the gift card, newest first: the sale and top-ups in, and spends at your stores out. Filter by
GET /api/v1/brand/pins/{voucher}/movementsdayand page withlimitandpage.Request
api GET "brand/pins/9f1c2e7a-4b22-4c10-9a3e-7d2f1b6c8a90/movements?limit=20"Response (one movement)
{ "ok": true, "data": [ { "id": "3c6bb90c-4dc9-4e01-bdf2-75e91199847d", "external_reference": "order-5001", "amount": 10000, "currency": "NGN", "balance_before": 0, "balance_after": 10000, "direction": "in", "type": "pin_sale", "status": "success", "mode": "test", "created_at": "2026-11-04T10:02:11.000000Z" } ] } -
Block, unblock or give a new code
Block a gift card to stop it being spent, for example when a customer reports it lost, and unblock it when that is resolved. If a code may have been seen by someone else, give the gift card a new code: the old one stops working at once, the balance is unchanged, and the customer is emailed the new code.
POST /api/v1/brand/pins/block POST /api/v1/brand/pins/unblock POST /api/v1/brand/pins/new-codeRequest
api POST brand/pins/block '{ "pin_id": "9f1c2e7a-4b22-4c10-9a3e-7d2f1b6c8a90" }' api POST brand/pins/unblock '{ "pin_id": "9f1c2e7a-4b22-4c10-9a3e-7d2f1b6c8a90" }'Response
{ "ok": true, "data": null } -
Activate a printed card
If you sell printed gift cards, look a card up by the number on it to see the amounts it can be loaded with, then activate it for the amount the customer pays. That sells a gift card exactly as above: the face value comes from your issuing balance, the customer is emailed their code, and the card number stops working. Printed cards are live stock, so these calls need your live key.
POST /api/v1/brand/cards/lookup POST /api/v1/brand/cards/activateRequest
api POST brand/cards/lookup '{ "card_number": "9412773018562049615" }' api POST brand/cards/activate '{ "card_number": "9412773018562049615", "amount": 15000, "external_reference": "card-7001", "first_name": "Jane", "last_name": "Doe", "email": "[email protected]" }'Response to the activation (key fields)
{ "ok": true, "data": { "pin_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "pin_mask": "7720-****-****-3318", "amount": 15000, "currency": "NGN", "mode": "live", "card_mask": "9412 •••• 615", "order_reference": "PVO-7K2M9QXA" } }
Follow gift cards with webhooks
Pass a webhook_id when selling, or set one later with Change a PIN's webhook, to receive pin.sold, pin.topped_up, pin.spent and pin.expired for that gift card. Webhooks
Sell in bulk with quotes
For a business buying many cards at once, such as staff gifts, send a quote instead of selling card by card. Send a quote with the product, the value and number of cards, and a discount up to the product's max_quote_discount. You get a pay_url to send the buyer; they pay on our hosted page, and the cards are then sent from your issuing balance to the people you listed in recipients, or to whoever the buyer chooses. Pass a webhook_id to hear quote.paid, quote.sent, quote.review, quote.expired and quote.cancelled. Quotes made here also show on your Campaigns page, and those made there in Your quotes.
What your customers can do themselves
Every gift card email and wallet pass links to the customer's card account, whoever sold the card. There, after signing in with a code sent to their email, they can check the balance, freeze the card, get a new code (one a day), keep the same code or set a daily limit, top it up by card (the payment goes to your wallet as a normal top-up), transfer it to someone else, and make family cards funded from it. Your webhook hears each change: pin.topped_up, pin.code_changed (source cardholder), pin.frozen, pin.unfrozen and pin.moved. Webhooks
Handle failures
Failed requests return "ok": false and an error object with a stable code; branch on the code, not the message. These are the ones specific to selling:
| Code | HTTP | When |
|---|---|---|
wrong_account_type |
403 | A brand key on a /reseller/ path, or the key belongs to an account that can't sell. |
issuing_disabled |
403 | Selling is switched off for your account. Contact your account officer. |
catalog_item_not_found |
404 | No product you sell has that catalog_id. |
amount_not_offered |
422 | A set_amounts product and the amount is not one of its denominations. |
amount_too_small |
422 | The amount, or the balance after a top-up, is below the minimum. |
amount_too_large |
422 | The amount, or the balance after a top-up, is above the maximum. |
wallet_balance_low |
402 | Your issuing balance holds less than the amount. |
duplicate_external_reference |
422 | You already used this external_reference. |
invalid_store_id |
422 | No active store of yours with that ID in this mode. |
pin_not_found |
404 | No gift card with that pin_id sold by you in this mode. |
topup_not_allowed |
403 | The product does not allow top-ups. |
pin_blocked |
403 | The gift card is blocked. |
pin_expired |
403 | The gift card has expired. |
Go live
- Complete KYC and sign your brand agreement, and ask for live API access to be enabled.
- Create a live API key and swap it into your integration. The requests themselves do not change.
- Fund your live issuing balance in each currency you will sell, and create your live webhooks.
Next steps
- Sell a PIN for every field of the sell call.
- Take gift cards at checkout to accept gift cards at your checkout.
- Go-live checklist to check your integration before you go live.