Exchange authorization code

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

EventTriggerDescription
authorization.CodeRedeemedsyncFired when the code is successfully exchanged.
authorization.CodeRedeemFailedsyncFired when the code is invalid or already redeemed.
Recent Requests
Log in to see full request history
TimeStatusUser Agent
Retrieving recent requests…
LoadingLoading…
📘

Eventsauthorization.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" }'
FieldTypeRequiredNotes
codestringYesExactly 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": "…" }
}
FieldUse
access_tokenBearer token for authenticated calls. Lives expires_in seconds
refresh_tokenExchange for a new access token at refresh
expires_inAccess token lifetime in seconds (typically 3600)
campaign_idCampaign scope of this session
localeLocale negotiated at session creation. Switch your UI to it
integration_contextEcho 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

It 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

StatustypeMeaningWhat to do
400code missing or not 43 charactersCheck your URL parsing
404authorization-code-not-foundNever existed, or expiredRestart the hand-off
409authorization-code-already-redeemedAlready spentRestart the hand-off
500authorization-code-lookup-failedLookup failedRetryable
500session-mint-failedSession could not be mintedRetryable

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.CodeRedeemed is published on success, authorization.CodeRedeemFailed on an invalid or already-used code.
  • GET /me is the cheap way to confirm the session took. See Get me.
Body Params

Exchanges a one-time code for a Cognito JWT. The code is invalidated after use.

string
required
length between 43 and 43

Single-use token previously issued by the generate endpoint.

Headers
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