Developers

SureCashBack API

Credit and spend cashback, sync your catalogue and request payments — from your point of sale, app or back office. Fourteen endpoints, one key, signed webhooks.

Base URL

Every request goes to the same host in both environments — your key's prefix, not the URL, is what selects Test or Live.

Testhttps://api-staging.surecashback.finclusion.aisend an sk_t_ key
Livehttps://api-staging.surecashback.finclusion.aisend an sk_l_ key

One host for both environments — the key is the switch

SureCashBack does not have a separate sandbox host. Test and Live share the URL above; the sk_t_ / sk_l_ prefix on your key selects the environment, and /v1/public/ping echoes back which one you are in so you can confirm before anything financial.

Quickstart

Four steps from nothing to a customer earning cashback.

  1. Create a key on API Keys. It is shown once — there is no endpoint that returns it again.
  2. Validate the key with /v1/public/ping. It echoes back the environment the key acts in, so you confirm you are pointed at Live or Test before anything financial.
  3. Prove the key with /v1/public/me before any write, so you know which merchant it acts as.
  4. Credit a customer with /v1/public/cashback/earn.
curl https://api-staging.surecashback.finclusion.ai/v1/public/me \
  -H "X-API-Key: sk_l_your_key_here"

Authentication

One header on every call. There is no unauthenticated endpoint.

header
X-API-Key: sk_l_xxxxxxxxxxxxxxxx

Authorization: Bearer sk_… also works. The bearer path is only taken when the token starts with sk_, so it cannot collide with a customer session token.

The key is shown once, at creation

It is stored hashed and there is no endpoint that returns it. Rotation issues a new key and invalidates the old one, so rotate deliberately — an in-flight integration using the old key stops at the moment of rotation.

Live vs Test

The prefix on your key decides which world you are acting in. Nothing else does.

PrefixEnvironmentActs onRate limit
sk_l_…LiveEvery endpoint on this page. Real balances600 / min
sk_t_…Test/v1/public/ping and /v1/public/me — nothing else120 / min

A test key cannot transact yet

A sk_t_ key reaches exactly two endpoints — /v1/public/ping and /v1/public/me. Every other endpoint on this page, earn and redeem included, refuses it with 403 ENVIRONMENT_NOT_PERMITTED.

This is not a problem with your key and retrying will not clear it. The sandbox partition is unfinished, so a test key is refused on anything that would write a record, notify a real customer, or return live rows.

So build against a live key. Use your own staff account as the test customer and small amounts — an earn can be reversed by an administrator, and every write is idempotent on your reference, so a retried call cannot double-charge while you are finding your feet.

Keys, webhook URLs and webhook secrets are all partitioned, and that partition is real — a Test webhook never receives Live activity. But because a test key cannot transact, it currently produces no deliveries to receive. Configure the Test webhook for the day the sandbox opens; verify your signature handling against Live.

Amounts

All money is kobo — integer minor units. There is no decimal money field anywhere in this API, in either direction.

You meanYou send
₦1.00100
₦1,000.00100000
₦12,345.671234567

Idempotency

Your reference is the idempotency key. It is not optional and it is not decorative.

Send a stable reference derived from your own transaction — a POS receipt id, an order number. Repeating it replays the original outcome.

On a repeat you get

  • The same reward — not a second one
  • replayed: true
  • No second webhook

Why no second webhook

A replay that fired a delivery would double-notify on every retry you make — and retries are exactly when replays happen.

Derive it from state, never from the clock

A reference built from a timestamp is a different key on every retry, which defeats the mechanism precisely when it is needed.

Endpoints

Fourteen calls — five for cashback, five for your catalogue and outlets, two for payment requests, plus identity and liveness.

Where it goes:body JSON request bodyquery URL query stringpath substituted into the URLheader HTTP header* required

One integration, every business type

This API is identical for a standard business, a corporate, a franchise, a bank or fintech, and an integration partner. A key resolves to a merchant identitythat carries no business-type field, so no endpoint, parameter or limit on this page varies by what kind of organisation you are. What differs between them is onboarding (which KYB documents you submit), your dashboard menus, and your commercial terms — none of which reach the wire.

Health check

API key
GET/v1/public/ping

Validates your API key and echoes back the environment it acts in. It is the safest call in the API — it reads nothing and writes nothing — which makes it the right first request from any new integration, and the right one to keep in a health check.

Headers

X-API-Key*sk_l_… / sk_t_…Required, like every endpoint. ping is one of only two paths a sk_t_ test key may reach
# Validates the key and echoes its environment. Safe: reads nothing, writes nothing.
curl https://api-staging.surecashback.finclusion.ai/v1/public/ping \
  -H "X-API-Key: sk_l_your_key_here"

Identify your key

API key
GET/v1/public/me

Returns the merchant identity your key resolves to, and whether it is active. Call it once after issuing a key and before any write — it is the only way to confirm a key acts as the merchant you think it does, and it costs nothing to be wrong here rather than on an earn.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
curl https://api-staging.surecashback.finclusion.ai/v1/public/me \
  -H "X-API-Key: sk_l_your_key_here"

List your outlets

API key
GET/v1/public/me/outlets

Lists the outlets registered to your merchant, each with the outletId that earn accepts for attribution. Use it to build the branch or store picker your POS sends. This response is paged, so a merchant with more than 25 outlets will not receive them all in one call.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
FieldInTypeNotes
pagequeryinteger1-based. Defaults to 1. Values below 1 are raised to 1 rather than rejected
pageSizequeryintegerDefaults to 25, clamped to 1–100. Asking for 500 silently gives you 100 — page, do not assume one call returns everything
curl https://api-staging.surecashback.finclusion.ai/v1/public/me/outlets \
  -H "X-API-Key: sk_l_your_key_here"

Read a customer’s rewards

API key
GET/v1/public/customers/{customerId}/rewards

Returns a single customer’s rewards position: what they have earned in total, what is spendable now, what is still pending, and what can be withdrawn. Read this before offering a redemption so you never present an amount the customer cannot actually spend.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
FieldInTypeNotes
customerId*pathuuidSubstituted into the URL, not sent as a query string. A non-UUID does not reach the handler — the route itself will not match
# availableMinor is what can be spent. totalEarnedMinor is NOT a balance.
curl https://api-staging.surecashback.finclusion.ai/v1/public/customers/{customerId}/rewards \
  -H "X-API-Key: sk_l_your_key_here"

Response

200 OK
{
  "totalEarnedMinor": 4500000,
  "availableMinor": 1250000,
  "withdrawableMinor": 1250000
}

totalEarnedMinor is not a balance

It is lifetime earnings. What can be spent is availableMinor; what can be cashed out is withdrawableMinor. Displaying the first as a balance overstates what the customer has.

Credit cashback (earn)

API key
POST/v1/public/cashback/earn

Records a purchase and credits the resulting cashback to a customer, funded from your own pool. This is the call your POS or checkout makes at the moment of sale. Your API key alone is sufficient here — a deliberate ruling rather than an oversight, because earn only ever credits the customer.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
Content-Type*application/jsonOmitting it returns 415 — this body is never form-encoded
FieldInTypeNotes
customerId*bodyuuidThe customer you are crediting
amountMinor*bodyintegerPurchase value in kobo — not the cashback. We compute the reward from it
reference*bodystringYour idempotency key. There is no Idempotency-Key header on this API — replay protection is this field
channelbodyenumOne of Pos, Qr, App, InStore, Card, Api
outletIdbodyuuidFrom /v1/public/me/outlets. An unrecognised outlet does not fail the earn — the reward is written with attribution visibly unresolved
terminalIdbodystringYour POS terminal identifier. Same forgiving behaviour as outletId
productReferencebodystringYour product/SKU reference
curl -X POST https://api-staging.surecashback.finclusion.ai/v1/public/cashback/earn \
  -H "X-API-Key: sk_l_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "00000000-0000-0000-0000-000000000000",
    "amountMinor": 1000000,
    "reference": "pos-txn-0001",
    "channel": "Pos"
  }'

Response

200 OK
{
  "reference": "pos-txn-0001",
  "rewardId": "01a08282-d8e7-7169-9cdc-94f93040a5ae",
  "cashbackMinor": 25000,
  "status": "Available",
  "availableAt": "2026-09-09T01:00:00Z",
  "expiresAt": "2027-09-09T01:00:00Z",
  "reasons": [],
  "replayed": false
}

Spend cashback (redeem)

Key + customer token
POST/v1/public/cashback/redeem

Applies a customer’s existing cashback against a purchase at your business. Because this moves the customer’s money rather than crediting it, your API key is deliberately NOT sufficient: the customer must also authorise the specific amount and payee from their own app.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
Content-Type*application/jsonOmitting it returns 415 — this body is never form-encoded
X-Txn-Authorization*the customer’s tokenMinted by the customer in their own app and bound to this amount and payee. Missing it returns 401 PIN_REQUIRED — your key cannot substitute for it
FieldInTypeNotes
customerId*bodyuuidMust be the same customer the token was minted by
amountMinor*bodyintegerKobo to redeem. Cannot exceed the customer’s availableMinor
reference*bodystringYour idempotency key. Same field, same rules as earn
descriptionbodystringShown on the customer’s statement
curl -X POST https://api-staging.surecashback.finclusion.ai/v1/public/cashback/redeem \
  -H "X-API-Key: sk_l_your_key_here" \
  -H "X-Txn-Authorization: <token the customer minted>" \
  -H "Content-Type: application/json" \
  -d '{
    "customerId": "00000000-0000-0000-0000-000000000000",
    "amountMinor": 50000,
    "reference": "pos-redeem-0001"
  }'

Response

200 OK
{
  "reference": "pos-redeem-0001",
  "appliedMinor": 50000,
  "replayed": false
}

The customer mints the token in their own app, bound to operation: "cashback.redeem", the amount and the payee (YOUR-MERCHANT-CODE). You relay it in X-Txn-Authorization. Without it: 401 PIN_REQUIRED.

403 REDEEM_NOT_ENTITLED is not an authorisation failure

It means the customer is not Tier 2, and that check runs before the authorisation check — so you never reached the gate you were testing. Verify their NIN first, then retry.

Look up an earn

API key
GET/v1/public/cashback/{reference}

Fetches one earn by the reference you sent, so you can answer “did that land?” without writing anything. Before this existed the only way to check was to re-POST the same reference and read replayed — a write used as a read, and indistinguishable in your logs from a genuine second attempt.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
FieldInTypeNotes
reference*pathstringThe reference you sent on earn
# "Did my earn land?" — scoped to your merchant, so a 404 means not yours or not there.
curl https://api-staging.surecashback.finclusion.ai/v1/public/cashback/pos-txn-0001 \
  -H "X-API-Key: sk_l_your_key_here"

Response

200 OK
{
  "reference": "pos-txn-0001",
  "rewardId": "01a08282-d8e7-7169-9cdc-94f93040a5ae",
  "customerId": "01a07fcb-f478-7645-9283-97ce508fb208",
  "paymentAmountMinor": 250000,
  "cashbackMinor": 8750,
  "reversedMinor": 0,
  "redeemedMinor": 0,
  "remainingMinor": 8750,
  "status": "Available",
  "channel": "Pos",
  "appliedRatePct": 3.5,
  "earnedAt": "2026-09-09T01:00:00Z",
  "expiresAt": "2027-09-09T01:00:00Z"
}

remainingMinor is the live figure

cashbackMinor is the historical gross earn and never moves — it is what reconciliation cross-foots against. What is still spendable after any reversal or redemption is remainingMinor.

A customer’s activity with you

API key
GET/v1/public/customers/{customerId}/transactions

Lists what a customer has earned at your merchant, newest first. Scoped to you deliberately: a partner who served a customer once does not thereby get their whole SureCashBack history.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
FieldInTypeNotes
customerId*pathuuidThe customer to read
pagequeryintegerDefaults to 1
pageSizequeryintegerDefaults to 25, clamped to 1–100
# This customer's earns AT YOUR MERCHANT only — not their whole history.
curl https://api-staging.surecashback.finclusion.ai/v1/public/customers/{customerId}/transactions?page=1&pageSize=25 \
  -H "X-API-Key: sk_l_your_key_here"

List your catalogue

API key
GET/v1/public/me/products

Your product catalogue, for syncing a point of sale. Optionally filtered to one category.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
FieldInTypeNotes
pagequeryintegerDefaults to 1
pageSizequeryintegerDefaults to 25, clamped to 1–100
categoryquerystringExact match. Omit for all categories
curl https://api-staging.surecashback.finclusion.ai/v1/public/me/products?page=1&pageSize=25 \
  -H "X-API-Key: sk_l_your_key_here"

Add a product

API key
POST/v1/public/me/products

Adds a product to your catalogue. Safe to call repeatedly: if you send an sku you have used before, the existing product is returned rather than a duplicate created.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
Content-Type*application/jsonOmitting it returns 415 — this body is never form-encoded
FieldInTypeNotes
name*bodystringProduct name
priceMinor*bodyintegerPrice in kobo
categorybodystringDefaults to General when omitted
skubodystringYour own identifier. Sending a repeated sku returns the existing product — this is what makes a catalogue re-sync safe
unitbodystringlitre, kg, each …
cashbackRatePctbodydecimalProduct-specific rate, if it differs from your default
cashbackCapMinorbodyintegerPer-transaction cap in kobo
# priceMinor is KOBO — 115000 = N1,150.00. Repeating an sku returns the existing product.
curl -X POST https://api-staging.surecashback.finclusion.ai/v1/public/me/products \
  -H "X-API-Key: sk_l_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name":"Premium Motor Spirit","sku":"PMS-1L","priceMinor":115000,"category":"Fuel","unit":"litre"}'

Read one outlet

API key
GET/v1/public/me/outlets/{id}

Fetches a single outlet, including its address and QR payload, without paging the whole list.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
FieldInTypeNotes
id*pathuuidThe outlet id
curl https://api-staging.surecashback.finclusion.ai/v1/public/me/outlets/{outletId} \
  -H "X-API-Key: sk_l_your_key_here"

Register an outlet

API key
POST/v1/public/me/outlets

Registers a new outlet under your merchant — for chains provisioning branches from their own systems rather than the dashboard.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
Content-Type*application/jsonOmitting it returns 415 — this body is never form-encoded
FieldInTypeNotes
name*bodystringBranch name
addressbodystringStreet address
citybodystring
statebodystring
phonebodystring
emailbodystring
curl -X POST https://api-staging.surecashback.finclusion.ai/v1/public/me/outlets \
  -H "X-API-Key: sk_l_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"name":"Ikeja Branch","address":"12 Awolowo Way","city":"Ikeja","state":"Lagos","phone":"+2348012345678"}'

Request a payment

API key
POST/v1/public/payments/request

Asks a customer to pay you. You already receive payment_request.paid on your webhook; this is the call that creates the request the event refers to.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
Content-Type*application/jsonOmitting it returns 415 — this body is never form-encoded
FieldInTypeNotes
amountMinor*bodyintegerKobo. Must be positive
customerPhonebodystringPhone or email is required — see below
customerEmailbodystringPhone or email is required — see below
customerReferencebodystringYour own reference. Does not satisfy the identifier requirement — it resolves against nothing
outletIdbodyuuidMust belong to you, or 400 OUTLET_NOT_FOUND
notebodystringShown to the customer
# amountMinor is KOBO — 250000 = N2,500.00. Phone OR email is required.
curl -X POST https://api-staging.surecashback.finclusion.ai/v1/public/payments/request \
  -H "X-API-Key: sk_l_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"amountMinor":250000,"customerPhone":"+2348012345678","note":"Table 4"}'

Phone or email is required

Without one, returns 400 CUSTOMER_IDENTIFIER_REQUIRED. A request with no reachable customer is not a payment request — it is a row that sits Pending until it expires, with nothing to explain why.

Read a payment request

API key
GET/v1/public/payments/requests/{id}

The current state of one payment request — the polling counterpart to the payment_request.paid webhook, for when you would rather ask than wait.

Headers

X-API-Key*sk_l_…Your secret key. May instead be sent as Authorization: Bearer sk_l_…
FieldInTypeNotes
id*pathuuidFrom the create response
curl https://api-staging.surecashback.finclusion.ai/v1/public/payments/requests/{id} \
  -H "X-API-Key: sk_l_your_key_here"

Webhooks

Seven events, signed, delivered to a URL you configure per environment.

Configure a URL and reveal your signing secret on Webhooks. You receive only what you subscribe to — subscribing to * keeps working when a new type is added, which is worth preferring to a list you would have to revisit.

EventFires when
reward.earnedCashback awarded — scan, POS, or the earn endpoint. Carries outletId, outletRef, productReference, subUnitId and subUnitRef when the caller stated them
reward.redeemedRewards spent at your business through the public API. Not an in-app redemption or a withdrawal — those have no merchant
cashback.reversedAn earn reversed or clawed back, only when the reversal names a payment reference. Carries cause — why it happened
payment_request.paidA payment request you raised was paid
settlement.completedA settlement run paid into your wallet. Carries gross, fee and net — only netMinor actually arrived
merchant.rating_submittedA customer rated you. Carries stars and hasCommentnever the comment text
customer.enrolledA customer joined one of your programmes
customer.onboardedYou signed a new customer up. Carries customerId only — a customer’s name and contact details are read through your dashboard, not pushed to your server
partner.staff_changedSomeone joined, changed role or left your team. Carries staffMemberId, change and role; key on the id, not the email
merchant.updatedYour own terms changed — cashbackRatePct, perTxnCapMinor or isActive. Worth subscribing to even if you ignore the rest: these decide whether a transaction you are about to submit is accepted

A missing subUnitRef means we were not told — not that the sale was not at a pump. Attribution is recorded only when the caller states it; we never infer it from whichever shift is open, and never correlate by amount and time. Send subUnitRef on the earn endpoint, or have the customer scan that sub-unit's own QR code.

A delivery

http
POST /your-webhook-endpoint
X-SCB-Event: reward.earned
X-SCB-Delivery: 01a0780c-4f2e-7a91-b3d5-9c1e8f2a6b07
X-SCB-Timestamp: 1788901234
X-SCB-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Content-Type: application/json

{ "event": "reward.earned", "reference": "pos-txn-0001", ... }

The signature covers {timestamp}.{rawBody} — not the bare body

That is what makes a captured delivery non-replayable, and it is the most common mistake made here.

# Recompute the digest by hand. BODY must be the RAW bytes as received —
# if you pretty-print or re-serialise the JSON first, this will not match,
# and that mismatch is the single most common false alarm here.

TS=1788901234                      # from X-SCB-Timestamp
SECRET='your_webhook_signing_secret'
BODY="$(cat delivery.json)"        # captured verbatim, not reformatted

printf '%s.%s' "$TS" "$BODY" \
  | openssl dgst -sha256 -hmac "$SECRET" -r \
  | awk '{print "sha256=" $1}'

# Compare with the X-SCB-Signature header. Equal => genuine and untampered.

Sign the raw bytes

Parsing and re-serialising the JSON changes them and breaks the digest. Capture the body before your framework touches it.

Deduplicate on X-SCB-Delivery

A delivery may arrive more than once. The delivery id is stable across retries; the event body is not a safe key.

Errors

RFC 9457 problem documents carrying a stable, machine-readable code.

Branch on code, never on title

Titles are human-facing copy and may change without notice. code is the contract.

CodeStatusWhat to do
UNAUTHENTICATED401Key missing, unknown or revoked. Check the header name and the prefix
PIN_REQUIRED401Redeem without a valid customer token. Have the customer re-authorise
REDEEM_NOT_ENTITLED403Customer is not Tier 2 + Active. Not retryable until they verify
ENVIRONMENT_NOT_PERMITTED403Almost always a sk_t_ key on an endpoint test keys cannot reach. Not retryable — use a Live key
429Rate limited. Back off and retry with the same reference

Rate limits

Per key, per minute, applied at the edge.

600/min for a live key, 120/min for a test key. Over the limit returns 429. Retrying a write with the same reference is always safe — that is what idempotency is for.

What does not exist

Listed so you do not build against it.

  • No public reversal endpoint — by design, not yet. cashback.reversed is an event you receive; there is no public route to cause one, and none is planned. Cashback, once granted, cannot be taken back by the merchant that granted it. Reversals originate only from SureCashBack administration — either a refund clawback, or the cancellation of a reward held for review — and a person makes that call. This is a deliberate customer protection: unlike earn, which only ever credits, a reversal removes value the customer already holds — so it is not something an API key can do. Requesting it will not change the answer; raise a refund through support.
  • No public customer lookup by email or phone. You must already hold the customerId.
  • No usable sandbox. A test key reaches /ping and /me and nothing else, so integration testing happens against Live.
  • No bulk earn. One call per transaction — which is also what keeps the idempotency key meaningful.