This guide explains how to add Profile API (PAPI) support to your existing Global Header and ACM (Toegangsbeheer) integration. It assumes you already have ACM authentication (OpenID Connect or SAML) and the Global Header widget working with setProfile (including active, loginUrl, logoutUrl, etc.).
By adding PAPI, the Global Header can automatically display the user's name, active identity, and available identities. Optionally, you can also enable identity switching (switch capacity) directly from the header.
Note: idpProfileToken (PAPI) replaces idpData. If you are currently providing profile data manually via idpData, you should remove it and use idpProfileToken instead. Do not use both.
Terminology: This guide refers to a single opaque token value as the "profile token". It appears under different names depending on context:
papi_token — the claim name inside the id_token JWT in an OpenID Connect integration (what your backend extracts)urn:be:vlaanderen:acm:papi_token — the SAML attribute name in a SAML integration (what your backend extracts)idpProfileToken — the property name in the widget's setProfile() call (what your frontend passes)This guide assumes you already have:
client_id, client_secret, registered redirect URIs for OIDC — or equivalent SAML configuration)setProfile configured (active, loginUrl, logoutUrl)To add PAPI support, you need:
papi scope activated — Request the papi scope in your ACM integration dossier. Without this, the papi_token claim will not be present in the ID token, and the profile features described in this guide will not work.Here is everything your team is responsible for. Each item links to the relevant section for details.
papi scope in your ACM integration dossier (Prerequisites)papi to the scope (e.g. scope=openid+profile+papi) and extract the papi_token claim from the id_token JWT (Section 2.1)urn:be:vlaanderen:acm:papi_token attribute from the SAML response (Section 2.2)idpProfileToken (the papi_token) in your existing setProfile call together with active: true — both must be in the same call (Section 3)idpData from your setProfile call if you were using it — idpProfileToken replaces it (Legacy note)setProfile({ active: false }) — no token needed (Section 2)Optional — if you want identity switching (switch capacity):
switchCapacityUrl to your setProfile call, pointing to your switch-capacity endpoint (Section 3)login_hint query parameter and redirects directly to the ACM authorization endpoint with it (Section 4)Everything else (rendering the profile UI and appending login_hint on identity switch) is handled by the Global Header widget.
The integration involves four parties:
flowchart TB
subgraph website ["Your Website (Browser)"]
direction LR
frontend["Your Frontend Code<br/>Calls setProfile() on each page load<br/>(active: true + papi_token, or active: false)"]
subgraph widgetBox ["Global Header Widget (v5)"]
widget["Login · Logout · Switch · Profile"]
end
frontend -- "setProfile()" --> widget
end
hostBackend["Your Backend<br/>(paths are yours to choose, e.g. /auth/login, /auth/callback, ...)"]
acm["ACM (Vlaams Toegangsbeheer)<br/>/op/v1/auth · /op/v1/token · /op/v1/logout · Profile API"]
widgetBox -- "navigates to URLs you configure<br/>(login, logout, optionally switch)" --> hostBackend
hostBackend -- "OIDC redirects + token exchange" --> acm
hostBackend -- "papi_token" --> frontendKey principles:
id_token received from ACM contains a papi_token claim that your backend extracts and makes available to the frontend. The widget navigates the user to URLs you configure on your backend.setProfile() on every page load. When the user is logged in, pass active: true together with the idpProfileToken. When the user is logged out, pass active: false (no token needed).setProfile().Relevant API documentation for the PAPI integration:
setProfile — configure login/logout/switch URLs and authentication stateidpProfileToken — pass the papi_token to populate the user's profile automaticallyThe profile token is the same opaque value regardless of how your application integrates with ACM. How you receive it depends on whether you use OpenID Connect or SAML.
Security note: The profile token must reach the browser (the widget runs client-side), but treat it as a bearer credential. Do not store it in
localStorageorsessionStorage, and avoid logging it. The token is scoped to the session — keep its exposure short-lived.
Your logout flow does not change. After logout, your frontend should call setProfile with active: false as before (no token needed).
For the underlying ACM OpenID Connect flow, see ACM's documentation.
Your existing login flow stays the same, with two changes:
papi scope to your authorization request (e.g. scope=openid+profile+papi). Scopes are space-separated (openid profile papi); in URLs, spaces are typically encoded as + or %20.papi_token from the id_token JWT returned by ACM after the token exchangesequenceDiagram
participant B as Browser
participant HB as Your Backend
participant ACM as ACM
B->>HB: GET /auth/login (your path)
HB-->>B: 302 Redirect
B->>ACM: GET /op/v1/auth?client_id=...&scope=openid+profile+papi&...
Note over B,ACM: User authenticates at ACM
ACM-->>B: 302 Redirect with ?code=...
B->>HB: GET /auth/callback?code=... (your path)
HB->>ACM: POST /op/v1/token (exchange code for tokens)
ACM-->>HB: { access_token, id_token }
Note over HB: Store tokens in session<br/>Extract papi_token claim from id_token
HB-->>B: 302 Redirect to appThe id_token is a JWT. When the papi scope is activated, it contains a papi_token claim. Extract this token and make it available to your frontend so it can be passed to the Global Header's setProfile method.
When Profile API support is activated for a SAML integration, ACM automatically includes an extra SAML attribute in the authentication response:
| SAML Attribute | Description |
|---|---|
urn:be:vlaanderen:acm:papi_token |
The profile token (same value as in OIDC) |
Your backend should extract this attribute from the SAML response after authentication and make it available to the frontend, just like you would with the papi_token claim in an OIDC flow. From the frontend's perspective nothing changes — you pass the extracted value as idpProfileToken in your setProfile call (see Section 3).
You already call setProfile with active, loginUrl, logoutUrl, etc. To enable the profile features, add idpProfileToken to your existing call:
idpProfileToken (required): the papi_token extracted from the id_token (see Section 2)switchCapacityUrl (optional): the URL on your backend that handles identity switching (see Section 4). Only needed if you want to support switch capacity.await globalHeaderClient.accessMenu.setProfile({
active: true,
loginUrl: "/auth/login",
logoutUrl: "/auth/logout",
switchCapacityUrl: "/auth/switch-capacity",
idpProfileToken: papiToken,
});
Important: idpProfileToken and active: true must be provided in the same setProfile call. Providing them in separate calls will not work.
API reference:
idpProfileToken
If you cannot use the profile token (e.g. due to constraints in a legacy integration), you can manually provide profile data via the idpData property. This approach is legacy and should not be used for new integrations. If you are already using idpData and are now switching to PAPI, remove idpData from your setProfile call and use idpProfileToken instead.
For reference, the idpData approach looks like this:
await globalHeaderClient.accessMenu.setProfile({
active: true,
loginUrl: "/auth/login",
logoutUrl: "/auth/logout",
switchCapacityUrl: "/auth/switch-capacity",
idpData: {
user: {
firstName: "John",
name: "Doe",
},
activeIdentity: {
capacity: "BUR",
loginHint: "eyJjYXBfaGludCI6IkJVUiJ9",
},
identities: [
{
capacity: "BUR",
loginHint: "eyJjYXBfaGludCI6IkJVUiJ9",
},
{
capacity: "EA",
onBehalfOf: {
type: "organisation",
name: "My Company",
identifier: "BE0428315871",
},
loginHint: "eyJjYXBfaGludCI6IkVBIiwiY29kZV9oaW50IjoiMDQyODMxNTg3MSJ9",
},
],
},
});
setProfile API reference: AccessMenuMethods.setProfile
A user may have multiple identities (e.g., citizen, employee of an organization). If you provide a switchCapacityUrl in your setProfile call, the Global Header displays the user's available identities in a dropdown. When the user clicks a specific identity, the widget appends a login_hint to the switchCapacityUrl and navigates there.
If you don't need identity switching, you can skip this section entirely — just don't add switchCapacityUrl to your setProfile call.
The switchCapacityUrl you pass to setProfile is the URL the Global Header navigates to when the user wants to switch identity. The widget automatically appends a login_hint query parameter based on the identity the user selected. For example:
/auth/switch-capacity?login_hint=opaque-token-value
Treat login_hint as an opaque value. Your backend should accept it from the widget and forward it unchanged to ACM, without inspecting, decoding, or constructing it.
Your backend receives the login_hint from the widget and forwards it directly to the ACM authorization endpoint. Do NOT go through the logout endpoint — ACM will detect the existing SSO session and switch the identity without requiring the user to re-authenticate.
sequenceDiagram
participant B as Browser
participant HB as Your Backend
participant ACM as ACM
Note over B: Widget appends login_hint<br/>to switchCapacityUrl
B->>HB: GET /auth/switch-capacity?login_hint=eyJ... (your path)
Note over HB: Clear auth-related session state
HB-->>B: 302 Redirect
B->>ACM: GET /op/v1/auth?login_hint=eyJ...&client_id=...&scope=...
Note over ACM: Detect SSO session<br/>Switch identity (no user interaction)
ACM-->>B: 302 Redirect with ?code=...
B->>HB: GET /auth/callback?code=... (your path)
HB->>ACM: POST /op/v1/token (exchange code for tokens)
ACM-->>HB: { access_token, id_token }
Note over HB: Store tokens (switched identity)
HB-->>B: 302 Redirect to appYour backend should:
login_hintACM documentation: Gericht aanmelden
If for some reason no login_hint is present on the request, your backend can fall back to a generic switch by redirecting to the ACM end-session endpoint with switch=true and your client_id. After ACM redirects back, redirect to the authorization endpoint — ACM will then show an identity selection screen. See the ACM switch documentation for details.
| Component | Test (TNI) | Production |
|---|---|---|
| ACM Authorization | https://authenticatie-ti.vlaanderen.be/op/v1/auth |
https://authenticatie.vlaanderen.be/op/v1/auth |
| ACM Token | https://authenticatie-ti.vlaanderen.be/op/v1/token |
https://authenticatie.vlaanderen.be/op/v1/token |
| ACM Logout | https://authenticatie-ti.vlaanderen.be/op/v1/logout |
https://authenticatie.vlaanderen.be/op/v1/logout |
| Widget Script | https://widgets.tni-vlaanderen.be/api/v2/widget/UUID/embed |
https://widgets.vlaanderen.be/api/v2/widget/UUID/embed |
setProfile API: AccessMenuMethods.setProfileProfileConfig: ProfileConfig