Integration Token Exchange
Token exchange trades an ordinary session Access Token you already hold for a restricted one, without a fresh
login. It is the natural fit when your application or backend already has a signed-in user and now needs to
call an integration on that user's behalf.
Read Integration Scopes first — it describes what the resulting
token contains and how a tenant admin configures one.
This page is about downscoping an azuma doa session token. Trading a Google or Apple ID token for an azuma doa session token is a different feature — see Social Token Exchange.
Both doors below require the scope to list the Token exchange mode and the tenant switch
Integration Scopes: Token Exchange to be on.
From an application
POST deviceBinding/token/v1/mobile/{applicationId}/exchange
A device-bound request, signed with the bound device's key, consuming a login challenge from the challenge API — the same credentials an ID login requires. Two credentials matter here and both are checked: the device assertion proves the caller holds the device, and the subject token proves there is a live session to downscope.
The inner payload:
| Field | Description |
|---|---|
id | The user's account ID. Must match the subject token's subject. |
subjectToken | The session Access Token being downscoped. |
scope | The configured scope to mint for, on its own. |
requestChallenge | A login challenge. |
deviceData | Optional device information. |
deviceBoundIntegrityVerificationData | Required only when the tenant has integrity validation enabled for token exchange. |
From a backend
POST token/v1/exchange on the Admin API, authenticated with client credentials carrying the
tenant_id_admin scope — see Client Credentials.
Use this when your own server received a session token from one of your users and needs a narrower token to pass onward, rather than forwarding the user's full session token.
Both doors accept only a Device Binding session token. A token from the Authorization Code flow is
refused with AuthUnauthorized, as is an integration token — there is no exchanging an exchanged token.
This route sits behind the tenant's admin IP allowlist. If your tenant has IP filtering enabled, add your
backend's egress addresses to it or every call is refused with Forbidden.
| Field | Description |
|---|---|
applicationId | The application the subject token was issued for. |
id | The user's account ID. Must match the subject token's subject. |
subjectToken | The session Access Token being downscoped. |
scope | The configured scope to mint for. |
There is no tenant field in the request, deliberately. The tenant is taken from the client you
authenticated as, and the user is then resolved inside that tenant — so a backend cannot mint a token for
another tenant's user.
The health-id-epa-integration scope is refused on this door. See
Health-ID ePA Integration.
The response
Both doors return the same shape:
{
"accessToken": "eyJhbGciOiJFUzUxMiIsImtpZCI6...",
"expiresIn": 660,
"scope": "partner-acme",
"tokenType": "bearer",
"issuedTokenType": "urn:ietf:params:oauth:token-type:access_token"
}
The response has no refreshToken field, by design. An integration token is re-minted, never renewed — and an
integration token can never itself be exchanged again.
Read expiresIn rather than assuming the configured lifespan: an exchanged token receives whichever is
shorter, the scope's configured lifespan or the remaining life of the token it came from. Nothing derived
outlives its source.
Lifetime and revocation
An exchanged token is tied to the session it was derived from, which governs when it disappears:
| Event | Effect on the integration token |
|---|---|
| Logout of the session it was minted from | Revoked with it. |
| Logout of a different session of the same user | Survives. |
| Global logout | Revoked, along with every other token for that user. |
| Revoking the integration token itself | Ends the whole parent session — its Access Token and its Refresh Token. Revoking any token in the family revokes the family. |
Removing the scope from the tenant configuration | No new tokens are minted; tokens already issued live until they expire. |
An exchanged token shares the identity of the session it came from, so revocation cannot be aimed at one half of the pair. If you revoke an integration token — say because it leaked — the user's session ends with it and they must sign in again. When you only want the integration token gone, prefer letting it expire: they are short-lived by design and are re-minted rather than renewed.
An exchange that is already in progress when the logout lands may still produce a token, because the new token is written after its parent has been read. Such a token is bounded by the cap — it can outlive the logout by at most the parent's remaining lifetime — but do not treat a successful logout as proof that no integration token for that session exists. Where it matters, verify at introspection rather than assuming.
Errors
| Code | Meaning |
|---|---|
ScopeNotAllowed | The scope is not configured, not configured for this door, the tenant switch is off, or more than one scope was requested. |
AuthUnauthorized | The subject token is not valid for this user, this application and this tenant, or it is not an ordinary device-binding session token. |
AuthLoginIntegrityDataMissing | The tenant requires integrity data for token exchange and none was supplied. |
Forbidden | Backend door only — the caller is not a client-credentials client, the client has no tenant, the user does not belong to that tenant, or the request came from an address outside the tenant's IP allowlist. |
TokenExchangeFailed | The exchange could not be completed. |
Requesting health-id-epa-integration can additionally fail with HealthIdKvnrBindingMissing or
HealthIdEpaTokenIssuanceFailed — see
Health-ID ePA Integration.
See the Admin-API and Device-Binding-API Swagger documentation (linked from the azuma doa overview) for the full request and response schemas.