Authentication
The platform uses two authentication methods, depending on who is calling:
| Method | Used For |
|---|---|
API Key (X-API-Key header) |
SDK clients, server-to-server calls, event tracking |
Bearer token (Authorization: Bearer header) |
People: the dashboard, and direct API calls made as a user |
The commands on this page run as written against the stack from the Quick Start, in one terminal, top to bottom. Each one uses the shell variables set by the ones before it.
Bearer Token Authentication
A bearer token is issued when a user logs in, and authenticates later requests as that user.
Where the login is checked depends on AUTH_PROVIDER:
local(the default, and what the Quick Start runs): the platform checks the email address and password against its own users. Its tokens last 12 hours (LOCAL_AUTH_TOKEN_TTL_MINUTES, default720). After 10 failed attempts within 15 minutes, the address answers423until the 15 minutes are up. The email address is matched whatever its letter case (A–Z), and every casing of it counts toward the same failed attempts.cognito: the credentials are passed to an AWS Cognito user pool, which issues an access token and a refresh token. Users are created by an administrator (Adding a user). The dashboard does not yet sign in with Cognito. Sign in through the API withPOST /api/v1/auth/token. SSO sign-in needsAUTH_PROVIDER=local.
Obtaining a Token
Log in with the account's email address and password. This saves the token in $TOKEN
and prints what else the response says:
LOGIN=$(curl -s -X POST localhost:8000/api/v1/auth/login \
-H 'content-type: application/json' \
-d '{"email":"admin@demo.com","password":"Demo1234!"}')
TOKEN=$(jq -r .access_token <<<"$LOGIN")
jq '{token_type, expires_in}' <<<"$LOGIN"
It prints "token_type": "bearer" and "expires_in": 43200, the token's lifetime in
seconds (12 hours). The response also carries the user's profile as user.
A wrong email address or password answers 401 with {"detail":"Invalid email or password"}.
POST /api/v1/auth/login takes JSON with email and password.
POST /api/v1/auth/token takes the same two as a form, with the email address in the
username field. That's the OAuth2 form the API's interactive documentation at
localhost:8000/api/v1/docs uses for its Authorize button.
Check Who You Are Logged In As
The profile also carries id, username, full_name, is_superuser, is_active and
auth_provider.
Using a Bearer Token
Pass the token in the Authorization header. This lists the experiments; the demo data
has three:
curl -s localhost:8000/api/v1/experiments/ \
-H "Authorization: Bearer $TOKEN" | jq -r '.items[].key'
It prints each experiment's key, including checkout_button_color.
The collection URL ends with a slash, /api/v1/experiments/. Without the slash the API
answers 307 Temporary Redirect, which curl doesn't follow, so nothing is printed.
Token Expiry, Refresh and Logout
With the local provider there is no refresh token: when a token expires, requests answer
401 and the user logs in again. POST /api/v1/auth/logout answers 204 so a client can
discard its token. The token itself stays valid until it expires, because nothing is
revoked on the server.
With AUTH_PROVIDER=cognito, the login also returns a refresh token, and
POST /api/v1/auth/refresh with {"refresh_token": "..."} exchanges it for a new access
token. If the refresh token has expired too, the user logs in again.
API Key Authentication
API keys are long-lived credentials for programs. Use them when:
- integrating an SDK into your application;
- making server-to-server API calls from a backend service;
- tracking events from a client application.
Creating an API Key
Creating a key takes a bearer token. This saves the new key in $KEY:
KEY=$(curl -s -X POST localhost:8000/api/v1/api-keys \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"name": "Production App Key", "description": "Used by the checkout service"}' \
| jq -r .key)
echo "${KEY:0:5}"
It prints eptk_, the start of every key. The key is shown only in this response, so store
it securely. If you lose it, create a new key and delete the old one. See
API Key Management for listing, deleting and rotating keys.
The request also accepts an optional "scopes" list. The one scope that is enforced is
sdk:ruleset, for server-side SDK use: GET /api/v1/sdk/ruleset,
POST /api/v1/tracking/evaluations and POST /api/v1/tracking/assign/batch (all beta)
refuse a key without it, or whose owner can no longer change feature flags or experiments.
Only users who can change feature flags (ADMIN, DEVELOPER, or a superuser) can create a key
with it; anyone else gets 403 and no key is created. Every other endpoint that takes an
API key accepts any active key, whatever its scopes; see
Scopes.
Using an API Key
Pass the key in the X-API-Key header on every request. This tracks a purchase in the demo
data's checkout_button_color experiment:
curl -s -X POST localhost:8000/api/v1/tracking/track \
-H "X-API-Key: $KEY" \
-H 'content-type: application/json' \
-d '{
"user_id": "user-123",
"experiment_key": "checkout_button_color",
"event_type": "purchase_completed",
"value": 49.99
}' | jq .event_type
It prints "purchase_completed", the stored event's type.
And this evaluates the demo data's beta_features flag for the same user:
curl -s -G localhost:8000/api/v1/feature-flags/evaluate/beta_features \
-H "X-API-Key: $KEY" \
--data-urlencode "user_id=user-123" \
--data-urlencode 'context={"plan":"pro"}' | jq .enabled
It prints true.
An API key is accepted only by the endpoints an SDK calls: tracking, flag evaluation and the flag ruleset. Everything else takes a bearer token.
Roles and Permissions
Every user has one of four built-in roles. The role decides which operations are available.
| Role | Description | Typical Users |
|---|---|---|
| ADMIN | Every resource; managing user accounts also needs the separate superuser flag | Platform owners, engineering leads |
| DEVELOPER | Create and manage experiments and feature flags | Engineers, product developers |
| ANALYST | Read experiments, flags and results; create reports | Data analysts, product managers |
| VIEWER | Reads experiments, flags and results; like every role, can create an API key, and an API key writes tracking data | Stakeholders, executives |
What Each Role Can Do
| Action | VIEWER | ANALYST | DEVELOPER | ADMIN |
|---|---|---|---|---|
| View experiments, flags and results | Yes | Yes | Yes | Yes |
| Create and change experiments | — | — | Yes | Yes |
| Create and change feature flags | — | — | Yes | Yes |
| Create their own API keys | Yes | Yes | Yes | Yes |
| View audit logs | Own | All | Own | All |
| Manage users | — | — | — | Only with the superuser flag |
Access to a feature flag is by role, not by who created it: a DEVELOPER can change any flag, and a VIEWER can change none, not even one they created.
To see a refusal, create a VIEWER account as the administrator:
curl -s -X POST localhost:8000/api/v1/users/ \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"email": "viewer@example.com", "username": "viewer", "password": "Viewer1234!", "role": "VIEWER"}' \
| jq '{email, role}'
It prints the new account's email and "role": "VIEWER". A password needs at least eight
characters and at most 72 bytes (UTF-8; most characters outside English take two or more
bytes), with an upper-case letter, a lower-case letter and a digit. A longer or weaker one
answers 422.
Log in as the viewer, and try to create a flag:
VIEWER_TOKEN=$(curl -s -X POST localhost:8000/api/v1/auth/login \
-H 'content-type: application/json' \
-d '{"email":"viewer@example.com","password":"Viewer1234!"}' | jq -r .access_token)
curl -s -X POST localhost:8000/api/v1/feature-flags/ \
-H "Authorization: Bearer $VIEWER_TOKEN" \
-H 'content-type: application/json' \
-d '{"key": "viewer-flag", "name": "Viewer Flag"}'
The API answers 403 Forbidden with {"detail":"You don't have permission to create feature flags"}.
Change Your Own Password
Every user changes their own password with POST /api/v1/users/me/password, which asks for
the current one. A wrong current password is refused:
curl -s -X POST localhost:8000/api/v1/users/me/password \
-H "Authorization: Bearer $VIEWER_TOKEN" \
-H 'content-type: application/json' \
-d '{"current_password": "Not-the-password1", "new_password": "Viewer5678!"}'
It answers 403 with {"detail":"The current password is incorrect."}. That is 403, not
401: the session is still signed in. Wrong current passwords count toward the same limit as
failed logins, so after 10 of them within 15 minutes both answer 423.
With the right one, the password changes and the response is an empty 204:
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8000/api/v1/users/me/password \
-H "Authorization: Bearer $VIEWER_TOKEN" \
-H 'content-type: application/json' \
-d '{"current_password": "Viewer1234!", "new_password": "Viewer5678!"}'
It prints 204. The new password follows the same rules as a new account's. Changing it
doesn't sign out other sessions: a token issued before the change keeps working until it
expires. To end them straight away, an administrator deactivates the account
("is_active": false).
Sending your own password to PUT /api/v1/users/{id} or PUT /api/v1/admin/users/{id} is
refused with 403, for administrators too. Those routes set another account's password:
a superuser resets a password there, without knowing the old one.
With the modules installed, admins can also create custom roles with fine-grained permissions, and grant temporary permissions to individual users. These are stored and shown, but no permission check reads them yet; routes check the base role (#891). See the RBAC API Reference.
Example Authenticated Requests
Create an Experiment (DEVELOPER or ADMIN)
An experiment needs at least one variant and one metric:
curl -s -X POST localhost:8000/api/v1/experiments/ \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{
"name": "Checkout Button Green",
"hypothesis": "A green button increases checkout completion",
"variants": [
{"name": "Blue Button", "is_control": true, "traffic_allocation": 50},
{"name": "Green Button", "traffic_allocation": 50}
],
"metrics": [
{"name": "Checkout completed", "event_name": "checkout_completed", "is_primary": true}
]
}' | jq '{name, status}'
The experiment is created in draft, with a key made from its name, which SDKs use to
find it.
Error Responses
401 Unauthorized
Returned when no credentials are sent, or they aren't valid:
It prints {"detail":"Not authenticated"}. An expired or malformed bearer token answers
{"detail":"Could not validate credentials"}, a missing API key {"detail":"API key missing"}
and an unknown or deleted one {"detail":"Invalid API Key"}.
403 Forbidden
Returned when the user is logged in but their role doesn't allow the operation, as in the
viewer's attempt above. The detail says what was refused.
Security Notes
- Never commit API keys or access tokens to source control.
- Rotate API keys regularly: at least every 90 days for production keys.
- Use separate API keys for each service or environment.
- Access tokens expire; do not store them long-term.
- In production, send all API traffic over HTTPS.