Global Header Profile API (PAPI) Integration Guide

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:

  • An approved integration dossier with ACM (client_id, client_secret, registered redirect URIs for OIDC — or equivalent SAML configuration)
  • A working authentication flow with ACM (OpenID Connect or SAML)
  • The Global Header widget embedded on your website, with setProfile configured (active, loginUrl, logoutUrl)

To add PAPI support, you need:

  1. 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.

  • Request the papi scope in your ACM integration dossier (Prerequisites)
  • Extract the profile token after a successful login:
    • OIDC: Add papi to the scope (e.g. scope=openid+profile+papi) and extract the papi_token claim from the id_token JWT (Section 2.1)
    • SAML: Extract the urn:be:vlaanderen:acm:papi_token attribute from the SAML response (Section 2.2)
  • Make the profile token available to your frontend (e.g. via an API endpoint or server-rendered into the page) (Section 2)
  • Pass idpProfileToken (the papi_token) in your existing setProfile call together with active: true — both must be in the same call (Section 3)
  • Remove idpData from your setProfile call if you were using it — idpProfileToken replaces it (Legacy note)
  • On logout, call setProfile({ active: false }) — no token needed (Section 2)

Optional — if you want identity switching (switch capacity):

  • Add switchCapacityUrl to your setProfile call, pointing to your switch-capacity endpoint (Section 3)
  • Implement a switch-capacity endpoint on your backend that reads the 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:

%%{init:{"theme":"dark"}}%% flowchart TB subgraph website ["Your Website (Browser)"] direction LR frontend["Your Frontend Code
Calls setProfile() on each page load
(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
(paths are yours to choose, e.g. /auth/login, /auth/callback, ...)"] acm["ACM (Vlaams Toegangsbeheer)
/op/v1/auth · /op/v1/token · /op/v1/logout · Profile API"] widgetBox -- "navigates to URLs you configure
(login, logout, optionally switch)" --> hostBackend hostBackend -- "OIDC redirects + token exchange" --> acm hostBackend -- "papi_token" --> frontend
%%{init:{"theme":"default"}}%% flowchart TB subgraph website ["Your Website (Browser)"] direction LR frontend["Your Frontend Code
Calls setProfile() on each page load
(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
(paths are yours to choose, e.g. /auth/login, /auth/callback, ...)"] acm["ACM (Vlaams Toegangsbeheer)
/op/v1/auth · /op/v1/token · /op/v1/logout · Profile API"] widgetBox -- "navigates to URLs you configure
(login, logout, optionally switch)" --> hostBackend hostBackend -- "OIDC redirects + token exchange" --> acm hostBackend -- "papi_token" --> frontend
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" --> frontend

Key principles:

  • Your backend handles all authentication flows (login, logout, and optionally switch capacity) via OpenID Connect with ACM. After login, the 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.
  • Your frontend calls 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).
  • The widget handles the UI (buttons, profile display) based on the profile configuration you provide via setProfile().

Relevant API documentation for the PAPI integration:

  • setProfile — configure login/logout/switch URLs and authentication state
  • idpProfileToken — pass the papi_token to populate the user's profile automatically

The 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 localStorage or sessionStorage, 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:

  1. Add the 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.
  2. Extract the papi_token from the id_token JWT returned by ACM after the token exchange
%%{init:{"theme":"dark"}}%% sequenceDiagram 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
Extract papi_token claim from id_token HB-->>B: 302 Redirect to app
%%{init:{"theme":"default"}}%% sequenceDiagram 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
Extract papi_token claim from id_token HB-->>B: 302 Redirect to app
sequenceDiagram
    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 app

The 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.

%%{init:{"theme":"dark"}}%% sequenceDiagram participant B as Browser participant HB as Your Backend participant ACM as ACM Note over B: Widget appends login_hint
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
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 app
%%{init:{"theme":"default"}}%% sequenceDiagram participant B as Browser participant HB as Your Backend participant ACM as ACM Note over B: Widget appends login_hint
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
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 app
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 app

Your backend should:

  1. Clear auth-related session state (tokens, identity claims) so the callback establishes a fresh auth context for the new identity. Whether you destroy the entire session or only rotate auth data depends on your application.
  2. Redirect directly to the ACM authorization endpoint with the login_hint

ACM 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