Credentials and tokens

The three credentials a retailer integration uses, and which endpoints accept each.

The platform issues tokens from Cognito, and every protected route validates a bearer JWT. A retailer integration deals with at most three credentials, and which one you send depends entirely on who the request is on behalf of.

CredentialRepresentsObtained bySent as
S2S tokenYour backend, acting as the retailerOAuth 2.0 client_credentialsAuthorization: Bearer <token>
End-user tokenOne signed-in customerSession exchange or magic linkAuthorization: Bearer <token>
No credentialAn anonymous visitor / guest buyerNothing, or an expired token

There is also an AWS SigV4 scheme guarding /internal/*. Retailers never use it. If you have not read the overview yet, start there.

Which endpoints need what

The three groups behave differently enough that it is worth internalising the split before you write any code.

Public — no credential at all. The whole catalog, campaign config, timeslots, voucher validation, and analytics ingestion. Scope comes from the x-campaign-id header, not from a token. These are safe to call from a browser.

Optional credential — guest-capable. The checkout routes and payment lookup. Send an end-user token and the customer gets their loyalty balance, memberships and login-gated discounts applied; send nothing and the same call succeeds as a guest at full price. A malformed or expired token is treated as "guest", not as an error — so a silently expired session degrades to guest pricing rather than a 401. If applying loyalty matters to you, verify the session with GET /me before building the basket.

Required credential. Everything under /me/*, plus voucher redemption and date changes, needs a valid end-user token. POST /session needs an S2S token. A missing or invalid token here is a 401.

Each endpoint page states its requirement in a table at the top.

Getting an S2S token

Your backend authenticates once with the OAuth 2.0 client credentials grant and reuses the token until it is near expiry.

curl -X POST https://auth.qup.systems/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$QUP_CLIENT_ID" \
  -d "client_secret=$QUP_CLIENT_SECRET"
{
  "access_token": "eyJraWQ…",
  "expires_in": 3600,
  "token_type": "bearer"
}

Then on every call:

Authorization: Bearer eyJraWQ…
🚧

Cache the token

Tokens live one hour. Fetch a new one when the current token has less than ~60 seconds left, not on every request — the token endpoint is rate-limited and a per-request token fetch doubles your latency for no benefit. Never ship client_secret to a browser or mobile app; the client credentials grant belongs on your server.

Scopes

Your client is provisioned with the scopes your integration needs. The one that matters for a retailer handing over its own customers is:

ScopeGrants
funtrips/session.createCreate and exchange session authorization codes — session exchange
funtrips/wallet.writeAward and spend loyalty tokens for any customer in your campaign — loyalty wallet

Other funtrips/* scopes exist for back-office and internal use. If a call returns 403, your token is valid but lacks the scope the route requires; that is a provisioning change on our side, not something you can fix in the request.

Your campaign is in the token

The S2S token carries a campaign claim, resolved from your client_id. That is why POST /session takes no campaign parameter — your credentials already say which campaign you are. One client_id maps to exactly one campaign; if you run several campaigns you get several client ids.

The end-user token

An end-user token is minted for one customer, in one campaign, and carries:

ClaimMeaning
subThe platform's user id for this customer
external_user_idYour identifier for them, stored verbatim
campaignCampaign scope, inherited from the session
localeThe negotiated language at session creation
integrator_paramsOpaque values you attached, passed through untouched

Access tokens are short-lived (≤ 60 minutes). Every mint endpoint also sets access_token and refresh_token HttpOnly cookies (Secure, SameSite=None) alongside the JSON body, so a browser front-end can rely on cookies and never touch the token in JavaScript. Native apps use the body values and store them in the platform keychain.

Refresh with POST /session/token/refresh before expiry; end the session with POST /auth/logout.

Two ways to get a customer signed in

flowchart TD
    A[Customer is signed in<br/>on your platform] -->|You have your own accounts| B[Session exchange<br/>POST /session → POST /session/token]
    C[Customer has no account<br/>with you] -->|We e-mail them a link| D[Magic link<br/>POST /auth/link/create → POST /auth/link/verify]
    B --> E[End-user token + cookies]
    D --> E

Use session exchange when your customers are already logged in to your platform — it is the flow this API was designed around, and the one you almost certainly want. Use magic link when the platform must establish identity itself, by mailing a one-time link.

Both end at the same place: an end-user token scoped to your campaign.


Did this page help you?