End-User authentication
The impersonation key and the user token that every End-User API request sends, and what the API refuses
Every End-User API request sends two credentials. An API key in the X-API-Key header identifies
your application. A JWT in the Authorization header identifies the user. The request has the
permissions of that user, and nothing more.
| Credential | Header | What it does |
|---|---|---|
| Impersonation key | X-API-Key | Identifies your application. The key must have the access type Impersonate user. |
| User token | Authorization: Bearer | Identifies the user. The email login flow issues it. |
The base URL of the End-User API is {BASE_URL}/api/v2/end-user.
Get an impersonation key
Create the key in the Seamless OS portal, under API tokens. Select the access type
Impersonate user. A key with the access type Full API access gets a 403 response from
every End-User operation.
An impersonation key alone reaches no data. Without a user token, the API answers 401. Thus
the key identifies your application, and the user token is the credential that you must protect.
Store the user token like a session, and send every request over HTTPS.
Get a user token
The email login flow has two steps. Both operations are public: they take no API key and no
user token. Both are rate limited, and both answer 429 Too Many Requests at the limit.
Start the login. The API sends a 6-digit code to the email address of the user:
curl -X POST "{BASE_URL}/api/v2/end-user/auth/email/start" \
-H "Content-Type: application/json" \
-d '{
"email": "emma.johnson@example.com"
}'const startEmailLogin = async (email) => {
const response = await fetch('{BASE_URL}/api/v2/end-user/auth/email/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to start login: ${error.message}`);
}
// Keep the nonce. The verify step needs it.
return response.json();
};The API answers 202 Accepted also for an unknown email address, so a caller cannot find out
which addresses exist. The response carries a nonce that names this login attempt:
{
"nonce": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"expiresIn": 300,
"createdAt": "2026-07-12T10:00:00Z",
"expiresAt": "2026-07-12T10:05:00Z"
}Then send the code together with the email address and the nonce:
curl -X POST "{BASE_URL}/api/v2/end-user/auth/email/verify" \
-H "Content-Type: application/json" \
-d '{
"email": "emma.johnson@example.com",
"nonce": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"code": "482913"
}'const verifyEmailLogin = async (email, nonce, code) => {
const response = await fetch('{BASE_URL}/api/v2/end-user/auth/email/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, nonce, code }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Login verification failed: ${error.message}`);
}
return response.json();
};On success the API answers with the token:
{
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 604800,
"userId": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}Send accessToken as a Bearer token on every later request. The token expires after
expiresIn seconds. When it expires, start a new login.
Send both credentials
curl "{BASE_URL}/api/v2/end-user/accounts/me" \
-H "X-API-Key: $API_KEY" \
-H "Authorization: Bearer $USER_TOKEN"const response = await fetch('{BASE_URL}/api/v2/end-user/accounts/me', {
headers: {
'X-API-Key': API_KEY,
Authorization: `Bearer ${userToken}`,
},
});
const user = await response.json();The response is the profile of the user that owns the token, with the customers that the user belongs to.
What the API refuses
| Request | Response | internalCode |
|---|---|---|
| A key with full API access, with or without a user token | 403 Forbidden | 4337 |
| An impersonation key with no user token | 401 Unauthorized | 4338 |
| A user token with no key, or with an unknown key | 401 Unauthorized |
The two login operations are the exception. They answer without a key and without a token.
The Platform API and a user token
The Platform API also accepts a user token beside a full-access key on a set of user-context operations. That set gets smaller as applications move to the End-User API, and a new application must not depend on it.