Validate a voucher code (pre-login)

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.

Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…
📘

Throttle20 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" }'
FieldTypeRequiredNotes
codestringYesThe 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"
  }
}
FieldMeaning
statusACTIVE or REVOKED
validWhether it can be redeemed right now
max_redemptionsTotal allowed
redemption_countUsed so far
redemptions_remainingLeft
reasonHuman-readable explanation. Present only when valid is false

Branch on valid, not on the status code

A 200 does not mean the code is usable.

status: ACTIVE does 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" is valid.

const { data } = await res.json();
if (data.valid) return accept(data);
return reject(data.reason);   // show the reason, do not invent one

reason 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

Nothing 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

StatustypeMeaningWhat to do
404voucher-not-foundNo such code in this campaignTell the customer the code is unknown
429ThrottledBack off and retry
500Unexpected faultRetryable

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.

Body Params

Check the status of a voucher code without redeeming it. Sent
anonymously from the storefront (typically during the signup flow,
before the user has a session). The campaign scope comes from the
x-campaign-id header.

string
required
length ≥ 1

The voucher code to check.

Headers
uuid
required

Campaign scope for this request. Filters content to the specified campaign.

string
enum
Defaults to application/json

Generated from available response content types

Allowed:
Responses

Language
URL
LoadingLoading…
Response
Click Try It! to start a request and see the response here! Or choose an example:
application/json
application/problem+json