Validates and redeems a voucher code within the authenticated user's campaign.
On success, increments the redemption counter and publishes a
voucher.VoucherRedeemed event for downstream discount processing.
The voucher itself carries no discount value — the campaign's discount type
configuration determines what action the redemption triggers.
Some campaign types require a partner credential supplied via the
X-User-Token header; when the campaign demands one and it is missing
or rejected, the redemption fails with HTTP 401 before the voucher
counter is decremented.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
Events —voucher.VoucherRedeemed
Why this endpoint exists
Redemption is where a code becomes a benefit for a specific customer. It increments the redemption counter and publishes an event that downstream systems act on.
The design point worth internalising: the voucher carries no value. The code is a trigger, not a token with a face value. What redemption does is defined by the campaign's discount type:
Campaign discount_type | What redeeming does |
|---|---|
LOYALTY | Credits tokens to the customer's wallet |
CLUB_MEMBERSHIP | Activates a membership |
VOUCHER | Unlocks a campaign-defined product discount |
So the same code shape means different things on different campaigns, and your storefront should read discount_type from campaign config to phrase the outcome correctly. "You received €5" and "your membership is active" are not interchangeable.
Request
curl -X POST https://api.acc.funtrips.io/v2/vouchers/redeem \
-H "x-campaign-id: $CAMPAIGN_ID" \
-H "Authorization: Bearer $END_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "code": "WELCOME2026" }'| Field | Type | Required | Notes |
|---|---|---|---|
code | string | Yes | The code to redeem |
The customer and campaign both come from the token. There is no user parameter.
Campaigns that credit an external system
Some campaigns must call a system outside the platform synchronously as part of redemption —
crediting a loyalty account the retailer holds themselves, for instance.
Campaigns connected to your own systems may need an extra credential here.Whether one is required, and what it looks like, is settled when your integration is
scoped — see Connecting to your own systems.
If it is short-lived, fetch it immediately before this call rather than at login.
The external call runs first, and only a successful one lets the redemption proceed. When it
is missing a credential or gets rejected, the redemption fails before the counter moves —
nothing is consumed, no event is published, and the customer can retry having lost nothing.
Campaigns with no external side-effect need nothing extra.
Response
200 OK:
{
"data": {
"code": "WELCOME2026",
"campaign_id": "3c1e5a90-7b2d-4f61-9a83-1d4e7f2b8c05",
"redemptions_remaining": 0,
"tokens_awarded": 500
},
"meta": { "correlation_id": "…" }
}| Field | Meaning |
|---|---|
code | The redeemed code |
campaign_id | Campaign scope |
redemptions_remaining | Redemptions left after this one |
tokens_awarded | Tokens credited to the wallet. null on non-loyalty campaigns |
tokens_awarded is the only field that tells you what the customer actually got, and it is only meaningful on LOYALTY campaigns. On membership and unlock campaigns it is null — read the outcome from GET /me/memberships or from the discount that appears on the next checkout.
As everywhere in the loyalty surface, tokens_awarded carries no unit: read token_unit from campaign config before rendering it as money or points.
Concurrency: the same code, two customers
Multi-redemption codes get raced — a shared code posted somewhere public, several customers at once.
Redemption is a conditional update: the counter only moves while redemption_count < max_redemptions and status = ACTIVE. Whoever loses the race gets 410 (voucher-exhausted), and there is no overselling.
For campaigns with a synchronous partner side-effect there is a subtler property worth knowing: the partner-side idempotency key is built from the campaign, code and pre-flight count — not from customer identity. So two different customers racing the same logical redemption produce the same partner key, and the partner dedupes it. At most one credit per logical redemption, however many callers race for it. Same-customer retries dedupe identically.
Practically: retrying after a failure is safe, and you cannot double-credit a partner by retrying.
Errors
| Status | type | Meaning | What to do |
|---|---|---|---|
404 | voucher-not-found | No such code in this campaign | Report unknown code |
409 | voucher-revoked | The code was revoked | Not usable — do not retry |
410 | voucher-exhausted | No redemptions remaining | Permanent. Do not retry |
422 | voucher-not-yet-valid | Before valid_from | Show the start date |
422 | voucher-expired | After valid_until | Permanent |
422 | voucher-type-not-configured | The campaign has no award configured for this voucher type | Report this to us — a configuration gap, not a customer error |
401 | voucher-redemption-effect-auth-required | The external system needs a credential that was not supplied | Supply it and retry |
401 | voucher-redemption-effect-auth-rejected | Partner rejected the credential | Refresh the partner token, retry |
401 | — | Funtrips session invalid | Re-authenticate |
502 | voucher-redemption-effect-failed | Partner could not record the redemption | Retryable with backoff |
500 | — | Unexpected fault | Retryable |
The three 401s are not the same problem
401s are not the same problemThis is the distinction most worth coding carefully, because the wrong branch sends a perfectly good session back through login:
voucher-redemption-effect-auth-required— no credential for the external system was sent. Supply one.voucher-redemption-effect-auth-rejected— that credential is stale or wrong. Refresh it and retry. The slug is separate precisely so you can distinguish "refresh" from "not supplied".- Bare
401, no slug — the Funtrips session is invalid. Re-authenticate.
Only the third means the customer is logged out.
410 versus 409
410 versus 409410 Gone means exhausted — the code was legitimate and is now used up. 409 means revoked — cancelled by an operator. Both are permanent; the customer-facing wording differs ("this code has already been used" versus "this code is no longer valid").
Notes
voucher.VoucherRedeemedis published on success, driving downstream discount processing. It is published only after the counter moves, so a failed redemption produces no event.- Validate first where you can.
POST /vouchers/validateis anonymous and lets you reject a bad code before asking the customer to sign in. - Unlock vouchers at checkout take a different path. Codes that discount a basket go in
voucher_codeson create checkout, where invalid codes are silently ignored. This endpoint is for redemptions that attach to the customer, not to a basket.
