Quick question? Chat with us
Other Sites:RibiPeople
RibiRewardsRewards infrastructure for Africa
Book a demo
Reward API
OverviewAPI DocsRewardsCard
Partner API — v1

API Reference

Use the RibiRewards Reward API to connect your product to reward catalogue, campaign and recipient issuance capabilities across supported African markets. Start with the quick guide below, then use the endpoint reference when you are ready to build.

Quick startCreate a campaign
Base URL: https://api.rewardscardafrica.com

Contents

OverviewQuick startAuthenticationIdempotencyRate limitsCORS & securityEndpointsGet catalogueGet categoriesCreate campaignSubmit recipientsGenerate bulk codesCampaign redemptionsWallet balanceList redemptionsRequest accessDashboard loginDashboard wallet balanceDashboard redemptionsResponse headersErrorsStatus referenceCountriesGlossaryChangelog

Need an API key?

Contact us — we'll have credentials to you within one business day.

tech@ribirewards.com

Overview

The RibiRewards Reward API uses the existing RewardsCard Partner API, a REST API served over HTTPS. All request and response bodies are JSON. It is designed for server-to-server integration — your backend calls the API, and your frontend never needs to know about it.

01

Pick categories

Fetch just a country's category list — no nested product dump. Tick which ones a campaign should be scoped to, or skip this for the full catalogue.

02

Create a campaign

One call sets the name, country, category scope, and a single amount that every code issued under it will share.

03

Submit recipients

Send a batch of names and emails against the campaign. Each recipient is emailed their code directly — there's no separate delivery step on your side.

Quick start

Get your first card issued in under 5 minutes.

  1. 1

    Get an API key

    Submit a key request or email tech@ribirewards.com. Keys are provisioned manually and are active within one business day.

  2. 2

    Check available categories

    Call GET /v1/categories?country=NG with your key. You will receive just the category list for that country — no product dump.

  3. 3

    Create a campaign

    Call POST /v1/campaigns with a name, the country, an amount, and — if you want to scope it — the categoryIds from the previous step. Save the id this returns — this is your campaignId.

  4. 4

    Submit recipients

    Call POST /v1/campaigns/:campaignId/recipients with a name + email for each person, and an Idempotency-Key. Every recipient is emailed their code directly — there is no separate delivery step.

  5. 5

    That's it

    Each recipient opens shop.rewardscardafrica.com, enters their code, and shops within whatever categories the campaign was scoped to. Check GET /v1/campaigns/:campaignId/redemptions to see who's actually redeemed.

Authentication

Every request (except POST /v1/request-api-key) requires your API key as a request header:

X-API-Key: your_api_key_here

How authentication works

On every authenticated request, the API verifies your key against the partner registry. The key must exist and have status ACTIVE. A BLOCKED key is treated the same as an invalid key — you receive a 422 either way.

  • Keys are long-lived — there is no expiry mechanism. Rotate your key by contacting tech@ribirewards.com if you believe it has been compromised.
  • Your API key is redacted from server logs at the network layer — it is never written to log storage, even at debug level.
  • Store your key in an environment variable or secrets manager. Do not hard-code it, commit it to a repository, or pass it through a browser.
  • If you need to call this API from a browser context, contact us to add your origin to the CORS allowlist. See the CORS & Security section.

Key status reference

StatusBehaviour
ACTIVEAll endpoints work normally.
BLOCKEDAll authenticated requests return 422. Contact tech@ribirewards.com to resolve.

Idempotency

POST /v1/campaigns/:campaignId/recipients requires an Idempotency-Key header on every call. Omitting it returns a 400 immediately, before any issuance is attempted.

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Why this matters

Network timeouts are normal. Without idempotency, a timeout leaves you not knowing whether a card was issued — and a naive retry would issue a second one. With idempotency, you always retry with the same key, and the API returns the original result regardless of whether the first attempt succeeded.

  • The Idempotency-Key header is required on every POST /v1/campaigns/:campaignId/recipients call. A 400 is returned if it is missing.
  • Keys are scoped per partner account. The same key value sent by two different partners does not conflict.
  • Keys expire after 24 hours. After expiry, the same key value can be reused — it will be treated as a new request.
  • Only successful and client-error responses are cached (status < 500). A 500 or 504 is never cached — you can safely retry.
  • The cached response is returned as-is, including the original status code. A cached 200 returns 200, not 201.
  • Backed by Redis when configured, so this is safe across multiple instances — falls back to an in-memory store only in environments without Redis provisioned (e.g. local development).

Recommended approach

JavaScript / Node.js
// Generate one key per issuance attempt.
// Tie it to your own order ID so you can reconstruct it on retry.
import { randomUUID } from "crypto";

const idempotencyKey = randomUUID();

// Store idempotencyKey alongside your order before calling the API.
// If the call times out, retrieve your stored key and retry with it.

Rate limits

The API uses two-tier rate limiting on authenticated routes. All rate limit headers follow the standard RateLimit-* format and are present on every response.

TierScopeWindowLimitApplies to
Edge (IP)Per IP address60 seconds300 requestsAll endpoints — runs before authentication
Partner (general)Per API key60 seconds120 requestsAll authenticated endpoints
Partner (redeem)Per API key60 seconds20 requestsPOST /v1/campaigns/:campaignId/recipients only
Key requestPer IP address15 minutes5 requestsPOST /v1/request-api-key only
Dashboard loginPer IP address15 minutes10 requestsPOST /v1/dashboard-login only

Daily spend cap

In addition to request-rate limits, every partner account has a daily ceiling on total redemption value per currency. This is separate from the request rate — you can be well within the rate limit and still hit the spend cap if you issue enough high-value cards in one day.

  • The cap is per-currency and per-partner — spending in NGN does not count against your ZAR cap.
  • The cap resets automatically at midnight UTC.
  • When the cap is hit, POST /v1/campaigns/:campaignId/recipients returns 429 with a message identifying the spend cap (not the request rate) as the reason — checked as one total for the whole submission before anything is sent.
  • Spend is only counted on confirmed successful issuance — a failed or timed-out attempt never counts against the cap.
  • Contact tech@ribirewards.com to raise your cap.

Handling 429 responses

  • Read the Retry-After response header — it contains the number of seconds to wait before retrying.
  • Always implement back-off. Do not poll without a delay.
  • Check the message field to distinguish a rate-limit 429 from a spend-cap 429 — the messages are different.

CORS & Security

CORS

  • Server-to-server calls (no Origin header) are never blocked by CORS — this is correct browser behaviour and is intentional.
  • Browser-originated cross-site calls are checked against an allowlist. By default the allowlist is empty — this API is designed for server-to-server use.
  • If you need to call this API from a browser context (e.g. your own dashboard), contact tech@ribirewards.com to have your origin added to the allowlist.

Security headers

  • HTTPS is enforced. The HSTS header (max-age=31536000; includeSubDomains; preload) is set on every response.
  • The X-Powered-By header is not present — framework fingerprinting is disabled.
  • Request bodies are capped at 100kb. Requests exceeding this limit are rejected before reaching validation.
  • Every request has a 15-second server-side timeout. If a downstream call hangs, the server closes the connection and returns 504 rather than leaving it open indefinitely.
  • Auth headers (X-API-Key, Authorization) and cookies are redacted from server logs. Request and response bodies are never logged.

Endpoints

MethodPathAuthDescription
GET/v1/reward-card-catalogue RequiredFetch the product catalogue for a country
GET/v1/categories RequiredCategory list for a country — no product dump
POST/v1/campaigns RequiredCreate a campaign — country, category scope, one amount
POST/v1/campaigns/:campaignId/recipients RequiredSubmit recipients (name + email) — the actual send
GET/v1/campaigns/:campaignId/redemptions RequiredRedemption activity for one campaign
GET/v1/wallet-balance RequiredGet your current wallet balance
GET/v1/reward-card-redemptions RequiredList all cards issued by your account
POST/v1/request-api-key NoneRequest API access (no key required)
POST/v1/dashboard-login NoneLog in with a dashboard key (not an API key)
GET/v1/dashboard/wallet-balance RequiredWallet balance, using a dashboard key
GET/v1/dashboard/reward-card-redemptions RequiredList redemptions, using a dashboard key
GET/v1/reward-card-catalogue Auth required

Returns the full product catalogue for one supported country, grouped by category. Only products with status ACTIVE and in-stock are included. Categories with no qualifying products are excluded from the response entirely.

Query parameters

FieldTypeRequiredDescription
countrystringRequiredThe country to fetch the catalogue for, e.g. NG, ZA, KE.Must be a 2-letter ISO 3166-1 alpha-2 code. Returns 422 if missing, 400 if unsupported.

Markup

If your account has a catalogue markup percentage configured, it is applied server-side before the response is built. The amount field on every product already reflects the marked-up price — you do not need to apply markup yourself, and you never see our base cost.

Example request

cURL
curl -X GET "https://api.rewardscardafrica.com/v1/reward-card-catalogue?country=NG" \
  -H "X-API-Key: your_api_key_here"
JavaScript
const response = await fetch(
  "https://api.rewardscardafrica.com/v1/reward-card-catalogue?country=NG",
  { headers: { "X-API-Key": "your_api_key_here" } }
);
const { country, currency, categories } = await response.json();
Python
import requests

resp = requests.get(
    "https://api.rewardscardafrica.com/v1/reward-card-catalogue",
    params={"country": "NG"},
    headers={"X-API-Key": "your_api_key_here"},
)
resp.raise_for_status()
data = resp.json()

Example response

200 OK
{
  "country": "Nigeria",
  "countryCode": "NG",
  "currency": "NGN",
  "categories": [
    {
      "id": "cat_a1b2c3d4-e5f6-...",
      "title": "Airtime & Mobile Data",
      "image": "https://cdn.example.com/categories/airtime.jpg",
      "products": [
        {
          "id": "prod_550e8400-e29b-...",
          "title": "MTN Nigeria ₦500 Airtime",
          "description": "Instant airtime top-up for any MTN Nigeria number.",
          "amount": "500.00",
          "currency": "NGN",
          "deliveryDays": "Delivered within a few hours",
          "image": "https://cdn.example.com/products/mtn-500.jpg",
          "brand": {
            "id": "brand_9c821f3a-...",
            "title": "MTN Nigeria",
            "image": "https://cdn.example.com/brands/mtn.png"
          }
        }
      ]
    }
  ]
}

Response fields — top level

FieldTypeNullableDescription
countrystringNoFull country name matching the requested country code.
countryCodestringNoThe 2-letter code you sent in the request, echoed back.
currencystringNoISO 4217 currency code for this country. All product amounts in this response are in this currency.
categoriesarrayNoArray of category objects, each containing its products. Empty arrays are never present — a category only appears if it has at least one qualifying product.

Response fields — category object

FieldTypeNullableDescription
idstring (UUID)NoStable category identifier. Safe to store and use as a reference key — categories are not reassigned or deleted, only deprecated.
titlestringYesDisplay name of the category, e.g. 'Airtime & Mobile Data'. Null in rare cases where a category record exists but has no title yet — always null-check before rendering.
imagestring (URL)YesCDN-hosted category image URL. Null when no image has been uploaded for this category. Do not hotlink — download and serve from your own CDN if you need guaranteed availability.
productsarrayNoAll active, in-stock products for this category in the requested country. Empty arrays are never returned — categories with zero qualifying products are filtered out before the response is built.

Response fields — product object

FieldTypeNullableDescription
idstring (UUID)NoStable product identifier. No longer passed directly in any API call — see campaignId in the glossary for how issuance actually works as of 25 Aug 2026.
titlestringYesProduct display name, e.g. 'MTN Nigeria ₦500 Airtime'. Null in rare cases — null-check before rendering.
descriptionstringYesShort product description. Null when not supplied by the catalogue source.
amountstring (decimal)YesFace value of the product expressed as a decimal string, e.g. '500.00'. Always a string — parse with parseFloat() before arithmetic. If your account has a catalogue markup configured, this value is already marked up; you do not need to apply markup yourself. Null in very rare legacy records.
currencystring (ISO 4217)YesISO 4217 currency code, e.g. 'NGN', 'ZAR'. Always matches the currency of the requested country — products cannot appear in a catalogue for the wrong currency.
deliveryDaysstringYesHuman-readable delivery expectation. This is a full display string, not a number — values include: 'Delivered within a few hours' (instant/voucher products), 'Usually delivered within 1–7 business days' (shop and ship), 'Concierge — booking confirmed by team' (experience products). Render as-is, do not append 'days' or 'business days'.
imagestring (URL)YesCDN-hosted product image URL. Null when no image exists. Apply the same hotlinking caution as category images.
brandobjectYesThe brand this product belongs to. Null in very rare cases where a product record has no associated brand. Always null-check the whole object before accessing sub-fields.
brand.idstring (UUID)NoStable brand identifier.
brand.titlestringYesBrand display name, e.g. 'MTN Nigeria'.
brand.imagestring (URL)YesCDN-hosted brand logo URL. Null when no logo has been uploaded.

Notes

  • Product IDs (id) are stable UUIDs. Safe to store as a reference to a specific product.
  • amount is always a string, not a number. Parse with parseFloat() or your language's decimal equivalent before any arithmetic.
  • deliveryDays is a full display string — do not append 'days' or modify it. Render it exactly as returned.
  • image URLs may be null at any level. Always null-check before rendering — do not assume an image exists.
  • The catalogue can change between calls (new products added, products going out of stock). Do not cache it indefinitely — refresh at least once per session or before showing it to a customer.
  • Country coverage expands periodically without notice. Re-confirm supported countries against GET /v1/reward-card-catalogue rather than hardcoding the list.
  • Every product returned here is voucher-fulfilled unless RibiRewards has separately agreed a physical/shop-and-ship arrangement with you. There is no field on the product object to filter or detect merchandise/physical products — that distinction isn't API-addressable today. If your integration needs to offer physical goods, contact tech@ribirewards.com to scope that separately; the recipient selects the product and delivery address on RibiRewards' own redemption page in that case, not inside your platform.
GET/v1/categories Auth required

Returns just the categories available in a country — id, title, image. Deliberately not the full catalogue: no nested products. Use this to build a category picker when creating a campaign, not GET /v1/reward-card-catalogue, which returns every product nested under every category.

Query parameters

FieldTypeRequiredDescription
countrystringRequiredThe country to list categories for.Must be a 2-letter ISO 3166-1 alpha-2 code matching a supported country.

Example request

cURL
curl -X GET "https://api.rewardscardafrica.com/v1/categories?country=NG" \
  -H "X-API-Key: your_api_key_here"
JavaScript
const resp = await fetch(
  "https://api.rewardscardafrica.com/v1/categories?country=NG",
  { headers: { "X-API-Key": "your_api_key_here" } }
);
const { categories } = await resp.json();
Python
import requests

resp = requests.get(
    "https://api.rewardscardafrica.com/v1/categories",
    params={"country": "NG"},
    headers={"X-API-Key": "your_api_key_here"},
)
resp.raise_for_status()
categories = resp.json()["categories"]

Example response

200 OK
{
  "country": "Nigeria",
  "countryCode": "NG",
  "categories": [
    { "id": "9f1c2e3a-1234-4a5b-8c9d-abcdef123456", "title": "Dining", "image": "https://cdn.rewardscardafrica.com/categories/dining.jpg" },
    { "id": "a1b2c3d4-5678-90ab-cdef-1234567890ab", "title": "Beauty", "image": "https://cdn.rewardscardafrica.com/categories/beauty.jpg" }
  ]
}

Response fields

FieldTypeNullableDescription
idstring (UUID)NoCategory identifier — this is what you pass in categoryIds when creating a campaign.
titlestringNoDisplay name of the category, e.g. 'Dining'. Returned only for categories that have at least one active, in-stock product in the requested country.
imagestring (URL)YesCDN-hosted category image. Null when no image has been uploaded.
POST/v1/campaigns Auth required

Creates a campaign — a name, a country, a category scope, and one amount that applies to every code issued under it. This replaces choosing a specific product per recipient: pick categories once here, then submit as many recipients as you like against the campaignId this returns. The partner attached to the campaign is always your own authenticated account — never a field you set yourself.

Request body

FieldTypeRequiredDescription
namestringRequiredYour own label for the campaign, e.g. 'August staff rewards'. Never shown to a recipient.Non-empty.
countrystringRequiredEvery code issued under this campaign is for this country.Must be a 2-letter ISO 3166-1 alpha-2 code matching a supported country.
amountnumberRequiredThe value of every code issued under this campaign. Set once here — there is no per-recipient amount.Must be a positive number.
categoryIdsarray of string (UUID)OptionalCategory IDs from GET /v1/categories. Omit for the full catalogue — recipients see every category available in the campaign's country.Must be an array if present.

Example request

cURL
curl -X POST "https://api.rewardscardafrica.com/v1/campaigns" \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "August staff rewards",
    "country": "NG",
    "amount": 500,
    "categoryIds": ["9f1c2e3a-1234-4a5b-8c9d-abcdef123456"]
  }'
JavaScript
const response = await fetch(
  "https://api.rewardscardafrica.com/v1/campaigns",
  {
    method: "POST",
    headers: {
      "X-API-Key": "your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "August staff rewards",
      country: "NG",
      amount: 500,
      categoryIds: ["9f1c2e3a-1234-4a5b-8c9d-abcdef123456"], // optional — omit for the full catalogue
    }),
  }
);

const { data } = await response.json();
const campaignId = data.id;
Python
import requests

resp = requests.post(
    "https://api.rewardscardafrica.com/v1/campaigns",
    headers={
        "X-API-Key": "your_api_key_here",
        "Content-Type": "application/json",
    },
    json={
        "name": "August staff rewards",
        "country": "NG",
        "amount": 500,
        "categoryIds": ["9f1c2e3a-1234-4a5b-8c9d-abcdef123456"],  # optional
    },
)
resp.raise_for_status()
campaign_id = resp.json()["data"]["id"]

Example response

201 Created
{
  "message": "Campaign created.",
  "data": {
    "id": "9f1c2e3a-1234-4a5b-8c9d-abcdef123456",
    "name": "August staff rewards",
    "country": "NG",
    "amount": "500.00",
    "categoryIds": ["a1b2c3d4-5678-90ab-cdef-1234567890ab"]
  }
}

Response fields

FieldTypeNullableDescription
idstring (UUID)NoCampaign identifier. This is what you pass as campaignId when submitting recipients or fetching campaign-scoped redemptions.
namestringNoYour own label for the campaign, e.g. 'TLC x MTN Ghana, Aug 2026'. Never shown to a recipient — for your own reference and reporting only.
countrystring (ISO 3166-1 alpha-2)NoThe country every code in this campaign is issued for. Set once at creation, applies to every recipient submitted against it.
amountstring (decimal)NoThe value of every code issued under this campaign. Set once at creation — there is no per-recipient amount.
categoryIdsarray | nullYesCategory IDs this campaign's codes are scoped to. Null or empty means unscoped — recipients see the full catalogue for the campaign's country.
sourcestring | nullYes'API' or 'DASHBOARD' — which auth path created this campaign. Set automatically, not something you pass. Null for campaigns created before this field existed.
POST/v1/campaigns/:campaignId/recipients Auth requiredIdempotency-Key required

The actual send. Submit up to 1000 recipients (name + email only — no per-row amount, the campaign's amount applies to every code) against a campaign you already created. Each recipient gets emailed their code directly — there is no separate delivery step. A malformed row never fails the whole submission; check the per-row results array.

Request body

FieldTypeRequiredDescription
recipientsarray of { name, emailAddress }RequiredEach entry needs a name and a valid emailAddress. Nothing else — amount and category scope come from the campaign.Non-empty array. Maximum 1000 entries.

Required headers

HeaderValueNotes
X-API-Keyyour_api_key_hereRequired on all authenticated endpoints.
Idempotency-KeyUUID v4 per attemptRequired — returns 400 if missing. A retried submission with the same key does not resend the batch.

Example request

cURL
curl -X POST "https://api.rewardscardafrica.com/v1/campaigns/9f1c2e3a-1234-4a5b-8c9d-abcdef123456/recipients" \
  -H "X-API-Key: your_api_key_here" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      { "name": "Lerisha Naidoo", "emailAddress": "lerisha@example.com" },
      { "name": "Sipho Dube", "emailAddress": "sipho@example.com" }
    ]
  }'
JavaScript
import { randomUUID } from "crypto"; // Node.js

const campaignId = "9f1c2e3a-1234-4a5b-8c9d-abcdef123456";

const response = await fetch(
  `https://api.rewardscardafrica.com/v1/campaigns/${campaignId}/recipients`,
  {
    method: "POST",
    headers: {
      "X-API-Key": "your_api_key_here",
      "Idempotency-Key": randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      recipients: [
        { name: "Lerisha Naidoo", emailAddress: "lerisha@example.com" },
        { name: "Sipho Dube", emailAddress: "sipho@example.com" },
      ],
    }),
  }
);

const { results, generatedCards } = await response.json();
// results has one entry per row — check status: "SENT" | "FAILED" per row,
// a partial failure does not fail the whole submission.
Python
import uuid, requests

campaign_id = "9f1c2e3a-1234-4a5b-8c9d-abcdef123456"

resp = requests.post(
    f"https://api.rewardscardafrica.com/v1/campaigns/{campaign_id}/recipients",
    headers={
        "X-API-Key": "your_api_key_here",
        "Idempotency-Key": str(uuid.uuid4()),
        "Content-Type": "application/json",
    },
    json={
        "recipients": [
            {"name": "Lerisha Naidoo", "emailAddress": "lerisha@example.com"},
            {"name": "Sipho Dube", "emailAddress": "sipho@example.com"},
        ],
    },
)
resp.raise_for_status()
data = resp.json()
results = data["results"]
generated_cards = data["generatedCards"]

Example response

200 OK
{
  "message": "2 of 2 codes sent.",
  "results": [
    { "row": 1, "emailAddress": "lerisha@example.com", "status": "SENT", "cardNumber": "RWDNG-8847-2291-XKQP" },
    { "row": 2, "emailAddress": "sipho@example.com", "status": "SENT", "cardNumber": "RWDNG-1122-3344-ABCD" }
  ],
  "generatedCards": [
    { "cardNumber": "RWDNG-8847-2291-XKQP", "emailAddress": "lerisha@example.com", "amount": "500.00", "currency": "NGN" },
    { "cardNumber": "RWDNG-1122-3344-ABCD", "emailAddress": "sipho@example.com", "amount": "500.00", "currency": "NGN" }
  ]
}

Response fields

FieldTypeNullableDescription
messagestringNoSummary of the submission, e.g. '48 of 50 codes sent.' Reflects results, not just acceptance — a 200 response can still contain per-row failures.
resultsarrayNoOne entry per row submitted, in the same order as the request. Each has row (1-indexed), emailAddress, status ('SENT' or 'FAILED'), and — on failure — reason. A malformed row never fails the whole submission.
generatedCardsarrayNoOne entry per successfully issued code: cardNumber, emailAddress, amount, currency. Only includes SENT rows — cross-reference against results if you need to see failures alongside successes.

Per-row validation and dedup

  • Deduplication happens per row within a campaign recipient submission — if the recipient's email already exists with an ACTIVE account, that account is reused rather than creating a duplicate.
  • A currency mismatch (the recipient's existing account is a different currency than the campaign's country) fails that row with a clear reason, rather than silently issuing the wrong currency.
  • There is no cross-request deduplication by design — submitting the same CSV twice against the same campaign issues a second batch of codes. Use a distinct Idempotency-Key per submission to guard against accidental retries.

Timeout and retry behaviour

  • The API has a 15-second server-side timeout. If a response does not arrive within 15 seconds, the server closes the connection and returns 504.
  • On a 504, some recipients in the batch may have already been sent — the internal call may have completed the submission before the timeout was hit on your end.
  • Always retry a 504 with the same Idempotency-Key rather than a fresh one — this returns the original result if the submission actually completed, instead of sending the batch a second time.
  • 500 responses are never cached by the idempotency store. You can always retry a 500.

Daily spend cap

  • Checked as ONE total for the whole submission — the campaign's amount × the number of recipients — before any code is generated, not per-recipient afterward.
  • If the total would exceed your cap, the entire submission is rejected with 429 and nothing is sent. Reduce the batch size or contact tech@ribirewards.com to raise your limit.
POST/v1/reward-cards/generate Auth required

Generates a batch of codes with no named recipient yet — for you to distribute yourself (CSV, your own UI, however fits your flow). No email is sent. This is the counterpart to submitting recipients above: use that endpoint when you already know who's getting a code, use this one when you just want a batch of codes in hand. Every code, either way, belongs to a campaign — there is no standalone/unscoped issuance path.

Request body

FieldTypeRequiredDescription
quantityintegerRequiredNumber of codes to generate in this batch.1–5,000
countrystring (ISO 3166-1 alpha-2)RequiredCountry these codes are issued for.Must be a supported country code
amountnumberRequiredValue of each code in this batch.Positive number
campaignIdstring (UUID)RequiredEvery batch belongs to a campaign — this field is required, not optional.Must be a campaign you own
categoryIdsarray | omitOptionalRe-picked on every call, not inherited from the campaign's original scope. Omit or send empty for the full catalogue.Category IDs from GET /v1/categories
notestringOptionalFor your own records only — not reviewed by anyone.Up to 1000 characters

Example request

cURL
curl -X POST "https://api.rewardscardafrica.com/v1/reward-cards/generate" \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "quantity": 10,
    "country": "NG",
    "amount": 500,
    "campaignId": "9f1c2e3a-1234-4a5b-8c9d-abcdef123456",
    "categoryIds": ["9f1c2e3a-1234-4a5b-8c9d-abcdef123456"]
  }'
JavaScript
const campaignId = "9f1c2e3a-1234-4a5b-8c9d-abcdef123456";

const response = await fetch(
  "https://api.rewardscardafrica.com/v1/reward-cards/generate",
  {
    method: "POST",
    headers: {
      "X-API-Key": "your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      quantity: 10,
      country: "NG",
      amount: 500,
      campaignId, // required — every batch belongs to a campaign
      categoryIds: ["9f1c2e3a-1234-4a5b-8c9d-abcdef123456"], // optional, re-picked each call
    }),
  }
);

const { codes } = await response.json();
// codes: [{ rewardCardId, cardNumber, serialNumber, amount, currency, expiredAt }]
// No email is sent — cardNumber is yours to distribute however you choose.
Python
import requests

campaign_id = "9f1c2e3a-1234-4a5b-8c9d-abcdef123456"

resp = requests.post(
    "https://api.rewardscardafrica.com/v1/reward-cards/generate",
    headers={
        "X-API-Key": "your_api_key_here",
        "Content-Type": "application/json",
    },
    json={
        "quantity": 10,
        "country": "NG",
        "amount": 500,
        "campaignId": campaign_id,  # required
        "categoryIds": ["9f1c2e3a-1234-4a5b-8c9d-abcdef123456"],  # optional
    },
)
resp.raise_for_status()
codes = resp.json()["codes"]

Example response

201 Created
{
  "message": "10 codes generated.",
  "codes": [
    {
      "rewardCardId": "rc_aaa111",
      "cardNumber": "RWDNG-8847-2291-XKQP",
      "serialNumber": "8847229 1XKQP",
      "amount": "500.00",
      "currency": "NGN",
      "expiredAt": "2029-09-08"
    }
  ]
}

Response fields

FieldTypeNullableDescription
rewardCardIdstring (UUID)NoInternal identifier for this code.
cardNumberstringNoThe redemption code itself. Not emailed to anyone — yours to distribute however you choose.
serialNumberstring | nullYesNon-secret trace identifier, separate from cardNumber. Safe to print in the clear (e.g. next to a scratch panel on a physical voucher).
amountstring (decimal)NoValue of this code, matching the amount you requested.
currencystring (ISO 4217)NoCurrency for this code's country.
expiredAtstring (date) | nullYesExpiry date. Inherits the campaign's expiryDate if set, otherwise a 3-year default.

Re-scoping an existing campaign

  • Country and category scope resolve per-campaign at redemption time, not per code-batch — there is no per-card scope. A generate call that would change a campaign's country or categories is only allowed while that campaign has no codes issued yet.
  • Once a campaign has any codes issued (via this endpoint or by submitting recipients), a further generate call with a different country or categoryIds is rejected with 409 — this is deliberate, not a bug. An in-place scope change would silently reshape what already-issued, possibly already-distributed codes can redeem.
  • To use a different scope, create a new campaign rather than trying to change an active one.

Daily spend cap

  • Checked as ONE total for the whole batch — amount × quantity — before any code is generated, same as recipient submission.
  • If the total would exceed your cap, the entire batch is rejected with 429 and nothing is generated.
GET/v1/campaigns/:campaignId/redemptions Auth required

Same shape as GET /v1/reward-card-redemptions, filtered to one campaign — see that endpoint below for the full field reference. Use this when you need redemption activity for a specific campaign rather than your whole account's history.

Example request

cURL
curl -X GET "https://api.rewardscardafrica.com/v1/campaigns/9f1c2e3a-1234-4a5b-8c9d-abcdef123456/redemptions" \
  -H "X-API-Key: your_api_key_here"
JavaScript
const campaignId = "9f1c2e3a-1234-4a5b-8c9d-abcdef123456";

const resp = await fetch(
  `https://api.rewardscardafrica.com/v1/campaigns/${campaignId}/redemptions`,
  { headers: { "X-API-Key": "your_api_key_here" } }
);
const { redemptions } = await resp.json();
Python
import requests

campaign_id = "9f1c2e3a-1234-4a5b-8c9d-abcdef123456"

resp = requests.get(
    f"https://api.rewardscardafrica.com/v1/campaigns/{campaign_id}/redemptions",
    headers={"X-API-Key": "your_api_key_here"},
)
resp.raise_for_status()
redemptions = resp.json()["redemptions"]
GET/v1/wallet-balance Auth required

Returns your current wallet balance. Balance is a computed value — the sum of all completed transactions on your account — not a raw stored field. The value returned is always accurate to the time of the request.

Example request

cURL
curl -X GET "https://api.rewardscardafrica.com/v1/wallet-balance" \
  -H "X-API-Key: your_api_key_here"
JavaScript
const resp = await fetch(
  "https://api.rewardscardafrica.com/v1/wallet-balance",
  { headers: { "X-API-Key": "your_api_key_here" } }
);
const { amount, currency } = await resp.json();
Python
resp = requests.get(
    "https://api.rewardscardafrica.com/v1/wallet-balance",
    headers={"X-API-Key": "your_api_key_here"},
)
data = resp.json()  # { "amount": 2450.00, "currency": "USD" }

Example response

200 OK
{
  "currency": "USD",
  "amount": 2450.00
}

Response fields

FieldTypeNullableDescription
currencystring (ISO 4217)NoISO 4217 currency code of your wallet, e.g. 'USD'. Your wallet is single-currency per the terms of the partnership agreement.
amountnumberNoCurrent balance as a number (not a string, unlike product amounts). Represents the total available to spend on card issuance. Decreases by the face value of each card issued (in your base wallet currency at the agreed rate).

Notes

  • amount is a number (not a string), unlike product and card amounts which are strings. This is consistent with how the underlying computation returns it.
  • Wallet currency is fixed per your partnership agreement. Contact tech@ribirewards.com if you need to top up or change the currency.
  • There is no pagination or history endpoint for wallet transactions at this time.
GET/v1/reward-card-redemptions Auth required

Returns all reward cards issued via your API key, with their current card status and fulfilment status. Use this to track deliveries, build your own reconciliation, or poll for status updates without implementing a webhook listener.

Example request

cURL
curl -X GET "https://api.rewardscardafrica.com/v1/reward-card-redemptions" \
  -H "X-API-Key: your_api_key_here"
JavaScript
const resp = await fetch(
  "https://api.rewardscardafrica.com/v1/reward-card-redemptions",
  { headers: { "X-API-Key": "your_api_key_here" } }
);
const { redemptions } = await resp.json();
const pending = redemptions.filter(r => r.fulfilmentStatus !== "ORDER_DELIVERED");
Python
resp = requests.get(
    "https://api.rewardscardafrica.com/v1/reward-card-redemptions",
    headers={"X-API-Key": "your_api_key_here"},
)
redemptions = resp.json()["redemptions"]
pending = [r for r in redemptions if r["fulfilmentStatus"] != "ORDER_DELIVERED"]

Example response

200 OK
{
  "redemptions": [
    {
      "rewardCardId": "rc_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "cardNumber": "RWDZA-8847-2291-XKQP",
      "recipientEmail": "lerisha@example.com",
      "amount": "500.00",
      "currency": "ZAR",
      "status": "ACTIVE",
      "orderNumber": "RCA-20260818-001",
      "fulfilmentStatus": "ORDER_PLACED",
      "issuedAt": "2026-08-18T10:23:00.000Z",
      "redeemedAt": null
    }
  ]
}

Response fields — redemption record

FieldTypeNullableDescription
rewardCardIdstring (UUID)NoIdentifier for the issued reward card. Use this to correlate redemption records with the generatedCards entries returned when you submitted recipients.
cardNumberstringNoThe redemption code, same as returned at issuance.
recipientEmailstringNoThe email address supplied at issuance.
amountstring (decimal)NoFace value of the card at issuance. String — parse before arithmetic.
currencystring (ISO 4217)NoCurrency of the card.
statusstring (enum)NoCard status — 'PENDING' (issued, not yet claimed by the recipient), 'ACTIVE' (claimed, not yet redeemed or expired), 'REDEEMED', 'EXPIRED', 'BLOCKED'. A freshly-issued card is PENDING until the recipient claims it on the redemption shop. Different from fulfilmentStatus: status reflects the card's own lifecycle; fulfilmentStatus reflects whether the physical or digital product has been delivered.
orderNumberstringYesHuman-readable order reference. Null if no fulfilment order was created (unusual — should always be present for partner-issued cards).
fulfilmentStatusstring (enum)YesCurrent fulfilment status. Null in the rare case where no order record is associated. See fulfilment status reference for all values and meanings.
issuedAtstring (ISO 8601)NoUTC timestamp of when the card was issued, e.g. '2026-08-18T10:23:00.000Z'.
redeemedAtstring (ISO 8601)YesUTC timestamp of when the recipient claimed the card. Null if not yet redeemed.

Notes

  • Results are scoped to your partner account only. You cannot retrieve cards issued by other partners.
  • There is no pagination at this time — all records are returned in a single response. Contact us if you need cursor-based pagination.
  • status and fulfilmentStatus are distinct. A card can be ACTIVE (not yet redeemed by the recipient) while its fulfilmentStatus is ORDER_DELIVERED (the product has been delivered).
  • Use this endpoint to poll for fulfilment updates rather than building a webhook listener — polling once every few minutes is sufficient for most fulfilment scenarios.
POST/v1/request-api-key No auth required

Request API access. This is the one endpoint that does not require authentication — it exists for integrators who do not have a key yet. Submitting this form notifies the RibiRewards team directly. Key provisioning is manual and typically takes one business day.

Request body

FieldTypeRequiredDescription
businessNamestringRequiredYour company or organisation name.Max 200 characters.
contactNamestringRequiredName of the primary technical contact.Max 100 characters.
contactEmailstringRequiredEmail address where credentials will be delivered.Must be a valid email. Normalised before storage.
notestringOptionalOptional — describe your integration, use case, or any specific requirements.Max 1,000 characters.

Example response

200 OK
{
  "message": "Request received. We'll be in touch at your contact email within 1 business day."
}

Notes

  • This endpoint is rate-limited to 5 requests per 15 minutes per IP to prevent spam. Every successful submission sends a real notification email.
  • If you need faster turnaround, email tech@ribirewards.com directly.
  • Key provisioning is manual — submission does not automatically create an account or key.
POST/v1/dashboard-login No auth required

For partners who use the partner dashboard (partner.rewardscardafrica.com) but never integrate with this API directly. Exchanges an email address and a dashboard key — a separate credential from your API key, issued by the RibiRewards team — for your partner details. This endpoint is itself the authentication check, so it does not require an X-API-Key header. A dashboard key cannot be used as an X-API-Key on any other endpoint, and an API key cannot be used here.

Request body

FieldTypeRequiredDescription
emailAddressstringRequiredThe email address your dashboard key was issued to.Must be a valid email.
dashboardKeystringRequiredYour dashboard key, as given to you by the RibiRewards team.None beyond presence — matched against the stored key.

Example request

cURL
curl -X POST "https://api.rewardscardafrica.com/v1/dashboard-login" \
  -H "Content-Type: application/json" \
  -d '{
    "emailAddress": "you@yourcompany.com",
    "dashboardKey": "your_dashboard_key_here"
  }'
JavaScript
const response = await fetch(
  "https://api.rewardscardafrica.com/v1/dashboard-login",
  {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      emailAddress: "you@yourcompany.com",
      dashboardKey: "your_dashboard_key_here",
    }),
  }
);

if (!response.ok) {
  const { message } = await response.json();
  throw new Error(message);
}

const { partnerId, partnerName, portalSlug } = await response.json();
Python
resp = requests.post(
    "https://api.rewardscardafrica.com/v1/dashboard-login",
    json={
        "emailAddress": "you@yourcompany.com",
        "dashboardKey": "your_dashboard_key_here",
    },
)
resp.raise_for_status()
data = resp.json()  # { "partnerId": "...", "partnerName": "...", "portalSlug": null }

Example response

200 OK
{
  "partnerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "partnerName": "Xoxoday",
  "portalSlug": null
}

Response fields

FieldTypeNullableDescription
partnerIdstring (UUID)NoYour partner account identifier. Not the same value as an API key — this is only meaningful within a dashboard-key session.
partnerNamestringYesYour business/company name as configured by the RibiRewards team. Null if not set.
portalSlugstringYesSet only for partners with a dedicated, isolated redemption portal (e.g. an embedded white-label shop). Null for the default shared shop. You do not need to do anything with this value directly — it exists for RibiRewards-side portal routing.

Notes

  • A wrong email, a wrong key, or a key that has been deactivated all return the same 422 and the same message — the response never reveals whether a given email is enrolled.
  • This endpoint is rate-limited to 10 requests per 15 minutes per IP — a separate, tighter budget from the general edge limit, since this is a login endpoint. See Rate limits.
  • A successful login here does not create a session or token — save the emailAddress and dashboardKey yourself and send both again as headers on every subsequent dashboard call (see the next two endpoints).
GET/v1/dashboard/wallet-balance Auth required

The dashboard-key equivalent of GET /v1/wallet-balance — identical response shape, same underlying computation. Authenticated with X-Dashboard-Email and X-Dashboard-Key headers instead of X-API-Key.

Headers

FieldTypeRequiredDescription
X-Dashboard-EmailstringRequiredSame email used to log in.
X-Dashboard-KeystringRequiredYour dashboard key.

Example request

cURL
curl -X GET "https://api.rewardscardafrica.com/v1/dashboard/wallet-balance" \
  -H "X-Dashboard-Email: you@yourcompany.com" \
  -H "X-Dashboard-Key: your_dashboard_key_here"
JavaScript
const resp = await fetch(
  "https://api.rewardscardafrica.com/v1/dashboard/wallet-balance",
  {
    headers: {
      "X-Dashboard-Email": "you@yourcompany.com",
      "X-Dashboard-Key": "your_dashboard_key_here",
    },
  }
);
const { amount, currency } = await resp.json();
Python
resp = requests.get(
    "https://api.rewardscardafrica.com/v1/dashboard/wallet-balance",
    headers={
        "X-Dashboard-Email": "you@yourcompany.com",
        "X-Dashboard-Key": "your_dashboard_key_here",
    },
)
data = resp.json()

Example response

200 OK
{
  "currency": "USD",
  "amount": 2450.00
}

Response fields

FieldTypeNullableDescription
currencystring (ISO 4217)NoISO 4217 currency code of your wallet, e.g. 'USD'. Your wallet is single-currency per the terms of the partnership agreement.
amountnumberNoCurrent balance as a number (not a string, unlike product amounts). Represents the total available to spend on card issuance. Decreases by the face value of each card issued (in your base wallet currency at the agreed rate).

Notes

  • Missing or invalid X-Dashboard-Email / X-Dashboard-Key headers return 422 — this route never falls through to anonymous access.
GET/v1/dashboard/reward-card-redemptions Auth required

The dashboard-key equivalent of GET /v1/reward-card-redemptions — identical response shape. Authenticated with X-Dashboard-Email and X-Dashboard-Key headers instead of X-API-Key.

Headers

FieldTypeRequiredDescription
X-Dashboard-EmailstringRequiredSame email used to log in.
X-Dashboard-KeystringRequiredYour dashboard key.

Example request

cURL
curl -X GET "https://api.rewardscardafrica.com/v1/dashboard/reward-card-redemptions" \
  -H "X-Dashboard-Email: you@yourcompany.com" \
  -H "X-Dashboard-Key: your_dashboard_key_here"
JavaScript
const resp = await fetch(
  "https://api.rewardscardafrica.com/v1/dashboard/reward-card-redemptions",
  {
    headers: {
      "X-Dashboard-Email": "you@yourcompany.com",
      "X-Dashboard-Key": "your_dashboard_key_here",
    },
  }
);
const { redemptions } = await resp.json();
Python
resp = requests.get(
    "https://api.rewardscardafrica.com/v1/dashboard/reward-card-redemptions",
    headers={
        "X-Dashboard-Email": "you@yourcompany.com",
        "X-Dashboard-Key": "your_dashboard_key_here",
    },
)
redemptions = resp.json()["redemptions"]

Response fields — redemption record

FieldTypeNullableDescription
rewardCardIdstring (UUID)NoIdentifier for the issued reward card. Use this to correlate redemption records with the generatedCards entries returned when you submitted recipients.
cardNumberstringNoThe redemption code, same as returned at issuance.
recipientEmailstringNoThe email address supplied at issuance.
amountstring (decimal)NoFace value of the card at issuance. String — parse before arithmetic.
currencystring (ISO 4217)NoCurrency of the card.
statusstring (enum)NoCard status — 'PENDING' (issued, not yet claimed by the recipient), 'ACTIVE' (claimed, not yet redeemed or expired), 'REDEEMED', 'EXPIRED', 'BLOCKED'. A freshly-issued card is PENDING until the recipient claims it on the redemption shop. Different from fulfilmentStatus: status reflects the card's own lifecycle; fulfilmentStatus reflects whether the physical or digital product has been delivered.
orderNumberstringYesHuman-readable order reference. Null if no fulfilment order was created (unusual — should always be present for partner-issued cards).
fulfilmentStatusstring (enum)YesCurrent fulfilment status. Null in the rare case where no order record is associated. See fulfilment status reference for all values and meanings.
issuedAtstring (ISO 8601)NoUTC timestamp of when the card was issued, e.g. '2026-08-18T10:23:00.000Z'.
redeemedAtstring (ISO 8601)YesUTC timestamp of when the recipient claimed the card. Null if not yet redeemed.

Notes

  • Results are scoped to your partner account only, same as the API-key version.
  • Missing or invalid X-Dashboard-Email / X-Dashboard-Key headers return 422 — this route never falls through to anonymous access.

Response headers

These headers are present on every response.

HeaderAlways presentDescription
x-request-idYesA UUID generated per request. Included in server logs — share this with support when reporting an issue to pinpoint the exact log entry. If you send an x-request-id header in your request, that value is used instead of a generated one.
content-typeYesAlways application/json; charset=utf-8. Every response, including errors, is JSON.
ratelimit-limitYesThe maximum number of requests allowed in the current window for the active rate limit tier.
ratelimit-remainingYesHow many requests remain in the current window before the limit is hit.
ratelimit-resetYesUnix timestamp (seconds) at which the rate limit window resets.
retry-after429 onlySeconds to wait before retrying. Present only on 429 responses. Always honour this value rather than implementing your own backoff.
strict-transport-securityYesmax-age=31536000; includeSubDomains; preload. HTTPS is required — HTTP requests are not accepted.
x-content-type-optionsYesnosniff. The API never serves HTML or mixed content.
x-frame-optionsYesDENY.

Errors

All error responses follow the same shape. The message field is always human-readable. Validation errors also include an errors array.

// Standard error
{ "message": "Human-readable description." }

// Validation error (422)
{
  "message": "Validation failed.",
  "errors": [
    { "type": "field", "msg": "Amount must be a positive number.", "path": "amount", "location": "body" }
  ]
}
400Bad Request

Cause: A field value is present but invalid — e.g. country code longer than 2 characters, amount not a positive number, or country does not match any supported country.

Response body
{ "message": "Selected country is not supported." }

Fix: Check the message field — it will identify the specific field and constraint that failed.

400 (missing Idempotency-Key)Missing required header

Cause: POST /v1/campaigns/:campaignId/recipients was called without an Idempotency-Key header. This header is required on every recipient-submission call.

Response body
{ "message": "Idempotency-Key header is required for this request." }

Fix: Add an Idempotency-Key header containing a UUID unique to this attempt. See the Idempotency section.

404Not Found

Cause: The requested path does not exist on this API.

Response body
{ "message": "Not found." }

Fix: Check the URL path and HTTP method. All endpoints live under /v1/.

422 (missing key)Missing API key

Cause: The X-API-Key header was not sent.

Response body
{ "message": "Missing X-API-Key header." }

Fix: Add X-API-Key: your_key to every request except POST /v1/request-api-key.

422 (invalid key)Invalid or inactive API key

Cause: The X-API-Key value does not match any known key, or the key's status is BLOCKED.

Response body
{ "message": "Invalid or inactive API key." }

Fix: Verify the key value. If the key is correct, contact tech@ribirewards.com to check its status.

422 (validation)Validation failed

Cause: One or more required fields are missing or fail format validation (email format, UUID format, field length, etc.).

Response body
{
  "message": "Validation failed.",
  "errors": [
    { "type": "field", "msg": "Amount must be a positive number.", "path": "amount", "location": "body" }
  ]
}

Fix: Check the errors array — each entry identifies the field, the constraint that failed, and the location (body or query). Fix each flagged field and retry.

429 (rate limit)Rate limit reached

Cause: You have exceeded the request rate for the active tier — either the IP-level edge limit or the per-partner limit.

Response body
{ "message": "Too many requests for this account. Please slow down and try again shortly." }

Fix: Wait for the number of seconds in the Retry-After response header before retrying. Do not poll without back-off.

429 (spend cap)Daily spend cap reached

Cause: Your account has reached the daily spend ceiling for one or more currencies. The cap resets at midnight UTC.

Response body
{ "message": "Daily spend cap reached for this account. Contact RibiRewards to raise your limit or try again tomorrow." }

Fix: Contact tech@ribirewards.com to raise the cap. The cap resets automatically at midnight UTC.

504Gateway Timeout

Cause: A downstream call to the fulfilment service took longer than 15 seconds. The card may or may not have been issued.

Response body
{ "message": "Request timed out." }

Fix: Retry with the same Idempotency-Key. If the card was issued on the first attempt, you will receive the original result; if not, a new card is issued. Never retry a timed-out redeem without an Idempotency-Key.

500Internal Server Error

Cause: An unexpected error occurred. The message is intentionally generic to avoid leaking internal state.

Response body
{ "message": "Something went wrong. Please try again later." }

Fix: Retry after a short delay. If the error persists, contact tech@ribirewards.com with the x-request-id from the response header.

Status reference

Fulfilment status

Reflects whether the physical or digital product has been delivered. Returned as fulfilmentStatus on each entry from GET /v1/reward-card-redemptions or GET /v1/campaigns/:campaignId/redemptions.

ValueMeaningTerminal?Auto-advances?
ORDER_PLACEDOrder received, pending fulfilment by the RibiRewards team.NoNo — updated by fulfilment team
PROCESSINGBeing prepared for dispatch or activation.NoNo — updated by fulfilment team
SHIPPEDPhysically dispatched (relevant for shop-and-ship products).NoNo — updated by fulfilment team
ORDER_DELIVEREDFully delivered and available to the recipient. For instant/voucher products this is set immediately at issuance.YesYes — instant/voucher products only
CANCELLEDCancelled — the card is no longer valid.YesNo — updated by fulfilment team
EXPIREDThe card's expiry date passed before redemption.YesNo — updated by fulfilment team

Card status

Reflects the lifecycle of the card itself — distinct from fulfilment status. Returned as status in the redemptions list.

ValueMeaning
PENDINGCard has just been issued; the recipient has not yet logged into the redemption shop with it.
ACTIVECard is valid and has not been redeemed or expired.
REDEEMEDThe recipient has claimed the card on the redemption shop.
EXPIREDThe card's expiry date has passed.
BLOCKEDThe card has been administratively blocked.

Supported countries

Country coverage expands without notice. Adding a new country requires a backend whitelist update — it is not solely a catalogue upload. Re-confirm the live list by calling the catalogue endpoint rather than hardcoding this table.

CountryCodeCurrency codeCurrency name
NigeriaNGNGNNigerian Naira
GhanaGHGHSGhanaian Cedi
EgyptEGEGPEgyptian Pound
MoroccoMAMADMoroccan Dirham
South AfricaZAZARSouth African Rand
KenyaKEKESKenyan Shilling
EthiopiaETETBEthiopian Birr
SenegalSNXOFWest African CFA Franc
AlgeriaDZDZDAlgerian Dinar
BotswanaBWBWPBotswana Pula
ZambiaZMZMWZambian Kwacha
Ivory CoastCIXOFWest African CFA Franc
ZimbabweZWZWLZimbabwean Dollar
RwandaRWRWFRwandan Franc
TanzaniaTZTZSTanzanian Shilling
UgandaUGUGXUgandan Shilling
TunisiaTNTNDTunisian Dinar
CameroonCMXAFCentral African CFA Franc
AngolaAOAOAAngolan Kwanza
MozambiqueMZMZNMozambican Metical
NamibiaNANADNamibian Dollar
MadagascarMGMGAMalagasy Ariary
MauritiusMUMURMauritian Rupee
TogoTGXOFWest African CFA Franc
BeninBJXOFWest African CFA Franc
NigerNEXOFWest African CFA Franc
Burkina FasoBFXOFWest African CFA Franc
MaliMLXOFWest African CFA Franc
Congo (DRC)CDCDFCongolese Franc

To request a specific market, email tech@ribirewards.com.

Glossary

campaignId
UUID of a campaign you created via POST /v1/campaigns. Every code issued through POST /v1/campaigns/:campaignId/recipients belongs to one campaign, which carries the country, amount, and category scope shared by every recipient in it.
rewardCardId / cardNumber
cardNumber is the actual redemption code — what a recipient uses to claim their reward. Returned per-recipient in generatedCards after a submission, and appears in redemption records.
categoryIds
Category UUIDs from GET /v1/categories, passed when creating a campaign. Scopes every recipient's shop to just those categories. Omit for the full catalogue.
fulfilment status
Whether the physical or digital product has been delivered (ORDER_PLACED, PROCESSING, SHIPPED, ORDER_DELIVERED, CANCELLED, EXPIRED). Distinct from card status.
card status
The lifecycle state of the card itself (PENDING, ACTIVE, REDEEMED, EXPIRED, BLOCKED). A card starts PENDING at issuance and becomes ACTIVE once the recipient claims it on the redemption shop. Distinct from fulfilment status.
idempotency key
A UUID you generate and include in the Idempotency-Key request header on POST /v1/campaigns/:campaignId/recipients. Guarantees that retrying a timed-out request does not resend the same batch of codes. Scoped per partner, expires after 24 hours, Redis-backed.
markupPct
A per-partner markup percentage configured by the RibiRewards team. If set, product amounts in the catalogue are already marked up in the API response — you do not apply markup yourself.
daily spend cap
A per-currency daily ceiling on total redemption value per partner. Checked as one total (campaign amount × recipient count) before a submission is processed. Resets at midnight UTC. Returns 429 when reached. Contact tech@ribirewards.com to raise it.
dashboard key
A separate login credential from your API key, for partners who use the partner dashboard (partner.rewardscardafrica.com) but never call the API directly. Issued by the RibiRewards team. Cannot be used as an X-API-Key — it only works with POST /v1/dashboard-login and the /v1/dashboard/* endpoints, including the dashboard-mounted versions of every campaign endpoint.
portalSlug
Returned by POST /v1/dashboard-login. Set only for partners with a dedicated, isolated redemption portal. Used internally for portal routing — most integrators can ignore this field.

Changelog

2026-09-08v1Initial release
  • New: POST /v1/reward-cards/generate — generate a batch of codes with no named recipient yet, for you to distribute yourself. No email is sent. This was previously undocumented here despite being live since 4 Sep 2026 — added now along with the behaviour changes below.
  • BREAKING: campaignId is now required on POST /v1/reward-cards/generate. Every code, generated or recipient-submitted, now belongs to a campaign — there is no standalone/unscoped issuance path.
  • New: optional categoryIds on POST /v1/reward-cards/generate, re-picked on every call rather than inherited from the campaign's original scope.
  • New: once a campaign has any codes issued (via either endpoint), a further generate call that would change its country or category scope is rejected with 409 rather than silently applied — country/category scope resolves per-campaign, not per-batch, so an in-place change would retroactively affect codes already issued under it. Start a new campaign for a different scope.
  • New: campaigns now carry a source field ('API' or 'DASHBOARD') recording which auth path created them. Null for campaigns created before this field existed.
2026-08-25v1Initial release
  • BREAKING: POST /v1/reward-card-redeem is retired. Category selection and one amount replace choosing a specific productId per recipient — nobody should have to browse a country's full product catalogue via an API call just to send one card.
  • New: GET /v1/categories — returns just a country's category list (id, title, image), not the full catalogue's nested product arrays. Use this to build a category picker.
  • New: POST /v1/campaigns — create a named campaign with a country, an amount, and an optional category scope. Everything issued under it shares these settings.
  • New: POST /v1/campaigns/:campaignId/recipients — submit up to 1000 recipients (name + email only) against a campaign in one call. Each gets emailed their code directly. Requires an Idempotency-Key, same protection the old redeem endpoint had. Per-row failures don't fail the whole submission.
  • New: GET /v1/campaigns/:campaignId/redemptions — redemption activity scoped to one campaign, same shape as the account-wide version.
  • New: recipient emails are now branded with your business name when available ("Acme sends you a reward card") instead of generic RibiRewards copy.
  • Fixed: daily spend cap documentation previously implied per-recipient checking — corrected to describe the real behaviour, which checks the whole submission's total before anything is generated.
  • Corrected: idempotency was previously documented as in-memory-only, needing Redis before multi-instance scaling. It is Redis-backed as of this release — corrected rather than left stale.
2026-08-21v1Initial release
  • New: optional campaignId field on POST /v1/reward-card-redeem. Reference a campaign (created by RibiRewards) to automatically scope the recipient's shop to specific categories, apply a campaign logo, and — if the campaign has a partner attached — restrict redemption to that partner's own portal. Omitting it behaves exactly as before.
  • New: dashboard-only login for partners who use the partner dashboard but never integrate with the API directly. POST /v1/dashboard-login exchanges an email + dashboard key (a separate credential from your API key, issued by RibiRewards) for your partner details. GET /v1/dashboard/wallet-balance and GET /v1/dashboard/reward-card-redemptions accept the same credential via X-Dashboard-Email / X-Dashboard-Key headers and return identical response shapes to their API-key equivalents. Has its own rate limit — see Rate limits.
  • Fixed: deduplication (same emailAddress + productId with an ACTIVE card) now actually returns the existing card instead of issuing a duplicate.
  • Fixed: expiryDate is now echoed back correctly — a UTC/local-time parsing bug could shift it a day earlier than the expiry you sent.
  • Fixed: wallet balance now decreases by a card's face value (converted to your wallet currency) at issuance — previously the debit was never recorded.
  • Fixed: giftLink now points to a working redemption page (shop.rewardscardafrica.com/redeem?code=...) instead of a dead link.
  • Fixed: catalogue amount now always renders with two decimal places (e.g. '500.00'), matching the redeem response instead of sometimes dropping them (e.g. '500').
  • Documented: card status can be PENDING (issued, not yet claimed by the recipient) in addition to ACTIVE/REDEEMED/EXPIRED/BLOCKED — this value already existed but was missing from the docs.
2026-08-18v1Initial release
  • Initial public release. Five endpoints: /v1/reward-card-catalogue, /v1/reward-card-redeem, /v1/wallet-balance, /v1/reward-card-redemptions, /v1/request-api-key.
  • Per-partner catalogue markup (markupPct) applied server-side — partners receive pre-marked-up prices.
  • Currency-aware daily spend cap — each currency tracked independently per partner.
  • Idempotency support on POST /v1/reward-card-redeem with 24-hour key TTL.
  • Three-tier rate limiting: IP edge limiter, per-partner limiter, stricter per-partner redeem limiter.
  • Structured request logging with per-request x-request-id. Auth headers and request bodies never logged.
  • Fulfilment order row created for every partner redemption — visible in admin with PARTNER: tag.

Ready to integrate?

Request API access and we'll have credentials and a sandbox endpoint to you within one business day.

Request API accessEmail us directly
RibiRewards

Rewards infrastructure for Africa.

PlatformOverviewCampaignsReward catalogueFulfilmentRecipient access
SolutionsConsumer incentivesChannel incentivesCustomer rewardsCustomer loyaltyTrade incentivesResearch incentivesReferral programmesManaged campaigns
ExploreIndustriesMarketsDevelopersResourcesCase studiesBlogCompanyCareers
LegalPrivacyTermsContact
© 2026 RibiRewards. All rights reserved.Built for Africa. Designed for scale.