Engineering docs
Merchant API — check and redeem Freebee coupons from your own system
Last updated Nov 4, 2026 · 6 sections
For merchants whose offers use codes Freebee generates. Your billing or POS system checks a code the shopper shows, redeems it once, and gets the same answer back on a retry. For offers that run on your own codes (a code pool or one shared code), you validate in your own system and send the redemptions back (the third call below).
Base URL: https://api.freebeeads.com (local: http://localhost:8001). Every call is POST
with a JSON body and the header X-Api-Key: fmk_…. Times are UTC in ISO 8601 with a Z.
Keys
The owner of your Freebee account creates keys on Settings → API keys (/settings/api):
- a key belongs to one brand and validates only that brand's coupons — another merchant's
code answers
not_valid_at_this_merchant; - optionally it is limited to some of your outlets: a redeem for another outlet answers
not_valid_at_this_outlet; - sandbox keys answer only for the sample offer below and never touch a real coupon;
- a revoked key is refused (
401) from its very next request; - after 10 checks answering
not_valid_at_this_merchantwithin 15 minutes the key is locked for 15 minutes (429withRetry-After) and Freebee Sales are alerted. The numbers are the platform settingcoupons.validation_lockout; - at most 60 requests a minute per key (
429withRetry-After).
The key is shown once at creation. Freebee keeps only a fingerprint of it.
1 · Check — POST /v1/merchant/coupons/check
Reads only. Checking a code a thousand times changes nothing.
curl -s https://api.freebeeads.com/v1/merchant/coupons/check \
-H 'X-Api-Key: fmk_your_key' -H 'Content-Type: application/json' \
-d '{"code": "FIZZ-7K3M-9PQA", "outlet_id": "optional-outlet-id"}'
{
"valid": true,
"status": "valid",
"message": "The code is valid and has not been used.",
"offer": {"campaign": "Fizz Spin", "brand": "Fizz Cola", "title": "₹100 off popcorn", "tier": "mid",
"issued_at": "2026-11-04T10:12:00Z", "expires_at": "2026-12-04T10:12:00Z"},
"conditions": {"terms": "Show at counter", "valid_until": "2026-12-04T10:12:00Z", "one_use": true,
"outlets": [{"id": "…", "name": "Inorbit kiosk"}]}
}
status is one of:
| status | valid | meaning |
|---|---|---|
valid | true | unused and in date |
already_redeemed | false | used before (at a counter, by this API, or reported by the merchant) |
expired | false | past the coupon's expiry |
voided | false | cancelled by Freebee Sales |
not_valid_at_this_merchant | false | unknown code, or another merchant's coupon (offer is null; counts toward the lockout) |
not_valid_at_this_outlet | false | the key is limited to other outlets |
merchant_codes_offer | false | the offer runs on your own codes; validate it in your system |
2 · Redeem — POST /v1/merchant/coupons/redeem
Marks the code used. Send a unique X-Request-Id (up to 80 characters) with every redeem.
If your call times out, retry with the same id: you get the same answer, and the code is
never redeemed twice. A second redeem of the same code under a new id answers
409 already_redeemed.
curl -s https://api.freebeeads.com/v1/merchant/coupons/redeem \
-H 'X-Api-Key: fmk_your_key' -H 'X-Request-Id: bill-20261104-000123' \
-H 'Content-Type: application/json' \
-d '{"code": "FIZZ-7K3M-9PQA", "outlet_id": "optional-outlet-id"}'
| HTTP | body |
|---|---|
200 | {"redeemed": true, "status": "redeemed", "redeemed_at": "…", "offer": {…}, "conditions": {…}} |
409 | {"redeemed": false, "status": "already_redeemed" / "expired" / "voided" / "not_valid_at_this_outlet" / "merchant_codes_offer", …} |
404 | {"redeemed": false, "status": "not_valid_at_this_merchant", …} |
422 | the X-Request-Id header is missing |
A replayed answer carries the header Idempotent-Replay: true.
There is no reverse action on this API. To undo a redemption, ask Freebee Sales (they reverse it on the dashboard and it is audited).
3 · Redemption data back — POST /v1/merchant/redemptions
For offers on your own codes. Send the codes you redeemed; Freebee matches each to the
coupon it was issued with and marks it redeemed, labelled merchant-reported in every report
and invoice. Up to 5,000 rows per call. Only code, redeemed_at and outlet are read;
anything else in a row is ignored and never stored.
curl -s https://api.freebeeads.com/v1/merchant/redemptions \
-H 'X-Api-Key: fmk_your_key' -H 'Content-Type: application/json' \
-d '{"rows": [
{"code": "FZ-00017", "redeemed_at": "2026-11-04", "outlet": "Inorbit kiosk"},
{"code": "FZ-00042", "redeemed_at": "2026-11-04 18:30", "outlet": "Online"}
]}'
{"total": 2, "matched": 1,
"unmatched": [{"row": 2, "code": "FZ-00042", "reason": "This code was never issued to a player."}]}
redeemed_at accepts YYYY-MM-DD, YYYY-MM-DD HH:MM, DD/MM/YYYY and DD-MM-YYYY; blank
means now. Reasons for an unmatched row: no such code in your offers · never issued to a
player · already reported earlier · already redeemed at the counter · the date could not be read
· a shared-code offer (one code for every winner cannot be matched to one coupon).
The same file can be uploaded on the campaign page (Coupon codes → Merchant redemption data)
as .csv or .xlsx with the columns code, redeemed_at (or date) and outlet (or channel).
Sandbox
Create a key with Sandbox ticked. It answers only for the sample offer Sandbox Café — One free filter coffee and keeps its own redeem state, so you can rehearse the whole flow:
| code | answer |
|---|---|
SBOX-OK01-TEST, SBOX-OK02-TEST, SBOX-OK03-TEST | valid until you redeem it with this key, then already_redeemed |
SBOX-USED-TEST | already_redeemed |
SBOX-EXPD-TEST | expired |
| anything else | not_valid_at_this_merchant (and counts toward the lockout, like live keys) |
Sandbox answers carry "sandbox": true. POST /v1/merchant/redemptions with a sandbox key
records nothing and lists every row back as unmatched.
Errors
| HTTP | when |
|---|---|
401 | missing, unknown or revoked key |
422 | the body is not valid JSON of the right shape, or X-Request-Id is missing on redeem |
429 | the key is locked after failed checks, or over 60 requests a minute (Retry-After seconds) |
Every answer is JSON; errors carry {"detail": "…"} in plain words.