Redeem a voucher code

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.

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

Eventsvoucher.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_typeWhat redeeming does
LOYALTYCredits tokens to the customer's wallet
CLUB_MEMBERSHIPActivates a membership
VOUCHERUnlocks 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" }'
FieldTypeRequiredNotes
codestringYesThe 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": "…" }
}
FieldMeaning
codeThe redeemed code
campaign_idCampaign scope
redemptions_remainingRedemptions left after this one
tokens_awardedTokens 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

StatustypeMeaningWhat to do
404voucher-not-foundNo such code in this campaignReport unknown code
409voucher-revokedThe code was revokedNot usable — do not retry
410voucher-exhaustedNo redemptions remainingPermanent. Do not retry
422voucher-not-yet-validBefore valid_fromShow the start date
422voucher-expiredAfter valid_untilPermanent
422voucher-type-not-configuredThe campaign has no award configured for this voucher typeReport this to us — a configuration gap, not a customer error
401voucher-redemption-effect-auth-requiredThe external system needs a credential that was not suppliedSupply it and retry
401voucher-redemption-effect-auth-rejectedPartner rejected the credentialRefresh the partner token, retry
401Funtrips session invalidRe-authenticate
502voucher-redemption-effect-failedPartner could not record the redemptionRetryable with backoff
500Unexpected faultRetryable

The three 401s are not the same problem

This 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 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.VoucherRedeemed is 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/validate is 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_codes on create checkout, where invalid codes are silently ignored. This endpoint is for redemptions that attach to the customer, not to a basket.
Body Params

Submits a voucher code for redemption within the authenticated campaign.

string
required
length ≥ 1

The voucher code to redeem.

Headers
uuid
required

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

string

Partner-issued user session token. Required for campaigns whose
discount type triggers a synchronous side-effect on a partner
system (e.g. Carrefour loyalty event). The token is short-lived
(≤5 minutes); requests received after expiry return 401.

string
enum
Defaults to application/json

Generated from available response content types

Allowed:
Responses

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