Skip to main content
The Session API lets you control which claims are included in the access tokens issued to your users. With a single configuration per application, you decide what data ends up in the JWT — pulled from the session, the user’s profile, or hardcoded values you define.

Why custom claims?

By default, an access token issued by Prelude only contains the standard JWT claims (iss, sub, exp, …) and the scopes granted to the session. If your backend needs more context — the user’s locale, IP address, your own internal user ID, a loyalty tier — you can declare it once in a claims mapping configuration and Prelude will inject it into every access token issued for that application. You can mix three kinds of values in the same configuration:
  • Hardcoded values — constants that always appear in the token (e.g. an API version number).
  • Built-in inputs — data Prelude already knows about the user or the session (user ID, IP, locales, emails, …).
  • Profile custom claims — arbitrary data you store on the user’s profile via the Management API.

How it works

You configure the mapping once per application through the Management API. From that moment on, every access token Prelude issues for that application — at login, refresh, or step-up — embeds the resolved claims.
When you update the claims mapping, existing sessions automatically pick up the new configuration on their next refresh. There is nothing to do on the client side.

Mapping format

A claims mapping is a free-form JSON object passed under the mapping key. Each top-level key becomes a claim in the access token. Nested objects are preserved as-is.
{
  "mapping": {
    "api_version": 2,
    "user_id": {
      "$input": "user_id",
      "$type": "uuid"
    },
    "loyalty_tier": {
      "$custom_claim": "loyalty_tier"
    },
    "context": {
      "ip": {
        "$input": "ip",
        "$type": "string"
      },
      "country": {
        "$input": "country_code",
        "$type": "string"
      }
    }
  }
}
This produces an access token whose payload includes:
{
  "iss": "https://<app_id>.session.prelude.dev",
  "sub": "...",
  "exp": 1713361200,
  "iat": 1713357600,
  "jti": "...",
  "sid": "ses_01kh1g2hp6ed7ar3ees1vxnkn7",
  "scope": "openid profile",

  "api_version": 2,
  "user_id": "019bd5d7-f977-76a5-a1ad-37260c9a7a3f",
  "loyalty_tier": "gold",
  "context": {
    "ip": "194.250.248.220",
    "country": "FR"
  }
}

Hardcoded values

Any non-object value (string, number, boolean) is copied verbatim into the token:
{
  "api_version": 2,
  "tenant": "production",
  "feature_flag_enabled": true
}

Built-in inputs

Use the $input / $type operators to reference data that Prelude already knows about the user and the session. Both operators are required together.
{
  "user_locales": {
    "$input": "locales",
    "$type": "string-array"
  }
}
The full list of supported inputs is in Available inputs below.

Profile custom claims

Any field stored on the user’s profile via PATCH /v2/session/apps/{appID}/users/{userID}/profile can be referenced with the $custom_claim operator:
{
  "loyalty_tier": {
    "$custom_claim": "loyalty_tier"
  }
}
If the referenced field is not present on the user’s profile, the claim is omitted from the token (rather than being set to null).
Profile claims are resolved at every token issuance, so updates to a user’s profile are reflected in the next access token without requiring re-authentication.

Nested claims

Mapping objects can be nested arbitrarily. Anything that is not a $input/$type or $custom_claim template is treated as a plain nested object:
{
  "context": {
    "device": {
      "ip": { "$input": "ip", "$type": "string" }
    }
  }
}

Available inputs

The following template names can be used with $input. Each input has a fixed set of supported $type values.
InputSupported typesDescription
user_iduuid, stringThe Prelude user ID.
session_iduuid, stringThe current session ID.
external_idstringThe user’s external ID, set via the Management API.
is_first_sessionbool, int, stringWhether this is the user’s first session.
ipstringThe IP address of the client when the session was created.
country_codestringISO 3166-1 alpha-2 country code derived from the IP address.
preferred_languagestringThe user’s preferred language, taken from the profile.
localesstring-array, stringThe list of locales known for the user (from social login or profile).
given_namestringThe user’s given name (from social login or profile).
family_namestringThe user’s family name (from social login or profile).
picturestringURL of the user’s profile picture (from social login or profile).
emailsstring-array, stringAll verified email identifiers attached to the user.
phone_numbersstring-array, stringAll verified phone identifiers attached to the user (E.164).

Type conversions

TypeBehavior
stringRenders the value as a string. Booleans become "true"/"false", numbers are stringified, arrays are joined with a single space.
uuidReturns the canonical UUID string representation of an ID. Only valid for user_id and session_id.
boolCoerces the value to a boolean. Strings "true"/"false" and non-zero numbers are accepted.
intCoerces the value to an integer.
string-arrayReturns the value as a JSON array of strings. A scalar input is wrapped in a single-element array.
If an input has no value at token issuance time (for example the user has no email identifier yet), the claim is omitted from the token.

Reserved claims

The following claims are reserved by the JWT specification and cannot be used as top-level keys in your mapping. They are managed by Prelude and would be stripped or rejected: iss · sub · aud · exp · nbf · iat · jti · sid · scope Attempting to set any of them at the root level returns 400 invalid_claim_override. They can still appear as keys inside nested objects (e.g. "metadata": { "iss": "..." }).

Managing the configuration

Each application can have at most one claims mapping configuration. The Management API exposes the standard CRUD operations.
1

Create the configuration

curl -X POST https://api.prelude.dev/v2/session/apps/${APP_ID}/config/claims \
  -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "mapping": {
      "user_id": { "$input": "user_id", "$type": "uuid" },
      "country": { "$input": "country_code", "$type": "string" },
      "loyalty_tier": { "$custom_claim": "loyalty_tier" }
    }
  }'
Returns 201 Created with the saved configuration. If a configuration already exists, the request fails with 409 claims_mapping_config_already_exists — use PUT instead.
2

Update the configuration

curl -X PUT https://api.prelude.dev/v2/session/apps/${APP_ID}/config/claims \
  -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "mapping": { ... }
  }'
Replaces the existing configuration entirely. Existing sessions pick up the new mapping on their next refresh.
3

Read the configuration

curl https://api.prelude.dev/v2/session/apps/${APP_ID}/config/claims \
  -H "Authorization: Bearer ${MANAGEMENT_API_KEY}"
Returns { "config": null } when no configuration exists.
4

Delete the configuration

curl -X DELETE https://api.prelude.dev/v2/session/apps/${APP_ID}/config/claims \
  -H "Authorization: Bearer ${MANAGEMENT_API_KEY}"
Returns 204 No Content. Future access tokens will only contain the standard JWT claims.

Errors

HTTP statusCodeWhen it happens
400invalid_requestThe request body is malformed, or a template object mixes incompatible operators (e.g. $input without $type, or $custom_claim together with $input).
400invalid_template_typeThe $input value is not a known template name, or the $type is not supported for that input (e.g. int for emails).
400invalid_claim_overrideA reserved JWT claim (iss, sub, aud, exp, nbf, iat, jti, sid, scope) appears at the root of the mapping.
409claims_mapping_config_already_existsPOST was called while a configuration already exists. Use PUT to update it.

Validation rules

When you submit a mapping, Prelude validates the structure before storing it:
  • A template object using $input must also include $type, and only those two keys.
  • A template object using $custom_claim must contain only that key.
  • $input, $type, and $custom_claim values must be strings.
  • The $input value must match one of the names in Available inputs.
  • The $type value must be one of the types listed for that input.
  • Reserved JWT claims cannot appear at the root level.
Hardcoded scalar values (strings, numbers, booleans) are not validated for shape — anything JSON-serializable is accepted.

Verifying tokens

Custom claims are signed alongside the standard JWT claims, so verifying a token has not changed: fetch the public keys from your application’s JWKS endpoint and verify the signature as usual. Once the signature is valid, the custom claims you configured can be read straight from the JWT payload.

What’s next?

JWKS

Verify the signature of access tokens issued by the Session API.

Claims Configuration API

Full API reference for managing the claims mapping configuration.