PUBLIC
Checks the status of a voucher code without redeeming it. Returns the
voucher's current state — status (ACTIVE/REVOKED), valid,
redemption_count, redemptions_remaining, and reason when invalid.
Anonymous: campaign is sourced from the x-campaign-id header so the
storefront can verify a code during the signup flow before the user
has a session. Voucher state is per-campaign rather than per-user, so
no caller identity is required.
The route is throttled at 20 RPS steady / 50 burst at the API Gateway
stage level to deter brute-force enumeration of the voucher code space.
Callers exceeding the cap receive HTTP 429.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
Throttle — 20 requests/second steady, 50 burst
Why this endpoint exists
The signup flow needs to check a code before the customer has an account. Someone arrives with a code on a card or in an e-mail, types it into your form, and expects to be told immediately whether it is any good — not after creating an account and reaching checkout.
Voucher state is per-campaign, not per-customer, so no caller identity is needed to answer the question. That is what makes an anonymous check possible.
It reads only: nothing is redeemed, no counter moves, no event fires. Safe to call as often as your form needs, within the throttle.
Request
curl -X POST https://api.acc.funtrips.io/v2/vouchers/validate \
-H "x-campaign-id: $CAMPAIGN_ID" \
-H "Content-Type: application/json" \
-d '{ "code": "WELCOME2026" }'| Field | Type | Required | Notes |
|---|---|---|---|
code | string | Yes | The code to check. Minimum 1 character |
Codes are campaign-scoped: the same string can exist in two campaigns with different state, so x-campaign-id is what makes the answer meaningful.
Uppercase and trim before sending. Codes are normalised on the platform's write path, but a leading space pasted from a spreadsheet is a needless 404.
Response
200 OK — the code exists, and valid says whether it can be used.
{
"data": {
"code": "WELCOME2026",
"campaign_id": "3c1e5a90-7b2d-4f61-9a83-1d4e7f2b8c05",
"status": "ACTIVE",
"max_redemptions": 1,
"redemption_count": 0,
"redemptions_remaining": 1,
"valid": true
},
"meta": { "correlation_id": "…" }
}An existing-but-unusable code is also 200, with valid: false and a reason:
{
"data": {
"code": "WELCOME2026",
"status": "ACTIVE",
"max_redemptions": 1,
"redemption_count": 1,
"redemptions_remaining": 0,
"valid": false,
"reason": "voucher has no remaining redemptions"
}
}| Field | Meaning |
|---|---|
status | ACTIVE or REVOKED |
valid | Whether it can be redeemed right now |
max_redemptions | Total allowed |
redemption_count | Used so far |
redemptions_remaining | Left |
reason | Human-readable explanation. Present only when valid is false |
Branch on valid, not on the status code
valid, not on the status code
A200does not mean the code is usable.
status: ACTIVEdoes not either — an active code with zero redemptions left is still unusable, and so is one outside its validity window. The single field that answers "can this be redeemed" isvalid.
const { data } = await res.json();
if (data.valid) return accept(data);
return reject(data.reason); // show the reason, do not invent onereason is written to be shown to a customer. Prefer it over your own guess, since it distinguishes cases your client cannot see — not yet valid, expired, exhausted, revoked.
valid: true is not a reservation
valid: true is not a reservationNothing is held. Between validating and redeeming, a single-use code can be spent by someone else — a shared code, or the same customer in another tab.
So handle a redemption failure even after a successful validation. Validation is a courtesy to the customer, not a guarantee to your client.
Throttling
The route is capped at 20 requests/second steady, 50 burst, at the gateway, specifically to make brute-force enumeration of the code space impractical. Exceeding it returns 429.
That is a generous budget for a signup form and a tight one for a script. To stay well inside it:
- Debounce the input — validate on blur or after ~500 ms of inactivity, not per keystroke.
- Require a plausible length before calling at all.
- On
429, back off and retry; do not hammer.
This is a shared limit across everyone using your campaign, so an undebounced form degrades the experience for other customers too.
Errors
| Status | type | Meaning | What to do |
|---|---|---|---|
404 | voucher-not-found | No such code in this campaign | Tell the customer the code is unknown |
429 | — | Throttled | Back off and retry |
500 | — | Unexpected fault | Retryable |
Note the split: a code that does not exist is 404, while a code that exists but cannot be used is 200 with valid: false. Your UI probably shows the same message for both, but the distinction is what lets you tell a typo apart from an exhausted code — worth logging separately.
Next step
A valid code is redeemed with POST /vouchers/redeem, which requires an authenticated customer. The usual sequence is: validate anonymously during signup → establish a session → redeem.
For unlock vouchers applied to a basket rather than to an account, pass the codes in voucher_codes on create checkout instead.
