PUBLIC
Exchanges a single-use authorization code for a Cognito JWT. The code is
invalidated immediately after use. Codes are issued by POST /session and
embedded in magic links sent to users.
Events fired
| Event | Trigger | Description |
|---|---|---|
authorization.CodeRedeemed | sync | Fired when the code is successfully exchanged. |
authorization.CodeRedeemFailed | sync | Fired when the code is invalid or already redeemed. |
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
Events —authorization.CodeRedeemed,authorization.CodeRedeemFailed
Why this endpoint exists
Step 3 of session exchange. Your backend minted a code and redirected the customer; the storefront now reads that code off its own URL and trades it for a real session.
It is unauthenticated because it cannot be otherwise — the caller is a browser that has no credential yet. The code is the credential, which is exactly why it is single-use and expires in minutes.
Call this from the front-end, not your server. The resulting tokens and cookies need to land in the customer's browser; fetching them server-side means inventing a second mechanism to forward them, which is the problem the code already solved.
Request
curl -X POST https://api.acc.funtrips.io/v2/session/token \
-H "Content-Type: application/json" \
-d '{ "code": "s7Qk2mXpR4vY8nB1tL6wZ3hJ9dF0gC5aE2iU7oS4rN8" }'| Field | Type | Required | Notes |
|---|---|---|---|
code | string | Yes | Exactly 43 characters, from the redirect URL |
The length is fixed and validated. A 400 here usually means your URL parsing mangled the value — a trailing fragment, a stray &, or double URL-decoding.
Response
200 OK, plus cookies.
{
"data": {
"access_token": "eyJraWQ…",
"refresh_token": "eyJjdHki…",
"token_type": "Bearer",
"expires_in": 3600,
"campaign_id": "3c1e5a90-7b2d-4f61-9a83-1d4e7f2b8c05",
"locale": "nl-NL",
"integration_context": { "loyalty_points_balance": 1250, "tier": "gold" }
},
"meta": { "correlation_id": "…" }
}| Field | Use |
|---|---|
access_token | Bearer token for authenticated calls. Lives expires_in seconds |
refresh_token | Exchange for a new access token at refresh |
expires_in | Access token lifetime in seconds (typically 3600) |
campaign_id | Campaign scope of this session |
locale | Locale negotiated at session creation. Switch your UI to it |
integration_context | Echo of what your backend supplied, so the front-end can confirm it |
Cookies
The response also sets:
Set-Cookie: access_token=…; HttpOnly; Secure; SameSite=None; Path=/
Set-Cookie: refresh_token=…; HttpOnly; Secure; SameSite=None; Path=/
A browser storefront can stop here. The cookies carry the session on subsequent requests and are HttpOnly, so JavaScript never touches the tokens — which is the safer posture. SameSite=None is what makes them work when the storefront is embedded in a frame on your domain.
Native apps ignore the cookies and store the body values in the platform keychain.
Do not do both. Pick cookies (web) or bearer headers (native) and be consistent, or you will eventually debug a request authenticated as a different customer than the one you expected.
locale is worth using
locale is worth usingIt reflects the Accept-Language your backend sent when minting the code — the customer's language according to your system, which is more reliable than the browser's preference. Switch the UI to it on receipt.
Errors
| Status | type | Meaning | What to do |
|---|---|---|---|
400 | — | code missing or not 43 characters | Check your URL parsing |
404 | authorization-code-not-found | Never existed, or expired | Restart the hand-off |
409 | authorization-code-already-redeemed | Already spent | Restart the hand-off |
500 | authorization-code-lookup-failed | Lookup failed | Retryable |
500 | session-mint-failed | Session could not be minted | Retryable |
The two failures your customers will actually hit
404 and 409 are the normal, expected outcomes of ordinary user behaviour — a bookmarked storefront URL with a stale code, a refreshed page re-submitting, a customer opening the link twice.
Both mean: silently restart the hand-off. Send the customer back to your own entry point, which mints a fresh code and redirects again. Do not show an error page; from the customer's point of view nothing went wrong, and a visible failure here reads as a broken integration.
const res = await fetch(`${BASE}/session/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // accept the cookies
body: JSON.stringify({ code }),
});
if (res.status === 404 || res.status === 409) {
window.location.href = YOUR_ENTRY_POINT; // mint a new code, redirect again
return;
}Strip the code from your URL after exchanging it — history.replaceState — so a refresh does not retry a dead code and so it does not sit in the address bar.
Notes
- The code dies on use, immediately, whether or not your client handled the response. A lost response means a lost code: restart the hand-off rather than retrying the same code.
authorization.CodeRedeemedis published on success,authorization.CodeRedeemFailedon an invalid or already-used code.GET /meis the cheap way to confirm the session took. See Get me.
