# Merchant API — check and redeem Freebee coupons from your own system

Last updated Nov 4, 2026 · Page: https://freebeeads.com/docs/repo/merchant-api · Markdown: https://freebeeads.com/docs/md/repo/merchant-api.md · All docs: https://freebeeads.com/llms-full.txt

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_merchant` within 15 minutes** the key is
  locked for 15 minutes (`429` with `Retry-After`) and Freebee Sales are alerted. The numbers are
  the platform setting `coupons.validation_lockout`;
- at most 60 requests a minute per key (`429` with `Retry-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.

```bash
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"}'
```

```json
{
  "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`.

```bash
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.

```bash
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"}
      ]}'
```

```json
{"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.
