Authentication
Learn how to authenticate your API requests with Flameup
Authentication Overview
Flameup uses API keys to authenticate requests. Each API key is scoped to a specific workspace and has granular permissions that control what operations it can perform.
API Key Format
Flameup API keys follow this format:
{prefix}.{secret}
Where:
- Prefix:
{type}_{env}_{workspace_short}_{random}(e.g.,ws_live_abc12345_abc123) - Secret: 64 hexadecimal characters
Full example:
ws_live_abc12345_abc123.a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
The prefix includes a shortened workspace ID for identification. Only the prefix is stored in readable form — the secret is stored as a hash, which is why a key cannot be retrieved after creation.
Two kinds of key
The prefix tells you where a key is allowed to live.
| Prefix | Where it belongs | What it can hold |
|---|---|---|
ws_live_ | Your server only. Backends, scripts, integrations | Any permission, including * |
pk_live_ | Inside your app or website. Shipped to users' devices | events:write, devices:register, people:identify — and nothing else |
These are the only two prefixes. The environment label on a key (live or
test) does not change them — a test-labelled key is still minted with a
ws_live_ or pk_live_ prefix.
A public key is restricted by the server, not by convention: a permission outside that list is rejected when the key is created and when it is updated, so one cannot be widened later.
ws_ key in a mobile app or a web page. A ws_ key can be granted permissions a public key cannot hold — reading your whole person table, sending messages to anyone — so a leaked one is only as limited as whatever it happens to carry. The pk_ prefix exists so a secret scanner can tell the two apart: allowlist pk_, alert on ws_.Authentication Methods
Authorization Header
Pass your API key in the Authorization header using the Bearer scheme. This
is the only method — there is no query-parameter or cookie alternative:
const response = await fetch(
'https://api.flameup.ai/api/v1/people',
{
method: 'GET',
headers: {
'Authorization': 'Bearer ws_live_abc12345_abc123.your_secret_here'
}
}
);
import requests
response = requests.get(
'https://api.flameup.ai/api/v1/people',
headers={
'Authorization': 'Bearer ws_live_abc12345_abc123.your_secret_here'
}
)
curl -X GET "https://api.flameup.ai/api/v1/people" \
-H "Authorization: Bearer ws_live_abc12345_abc123.your_secret_here"
What an API key can reach
An API key reaches the ingestion and messaging surfaces. Everything else in Flameup is built in the dashboard and authenticated with your dashboard session, not a key.
With an API key:
| Surface | Routes |
|---|---|
| Ingestion | POST /identify (people:write or people:identify), /track, /track/batch (events:write) |
| People | GET/POST/PUT/DELETE /people, /people/{id}, /people/search, /people/upsert, /people/batch |
| Events | GET /people/{external_id}/events, GET /events/search |
| Devices | POST/DELETE/GET /people/{external_id}/devices |
| Push | POST .../push/send, /send/batch, POST .../push/tokens/validate, POST .../push/tokens/suppress, DELETE .../push/tokens/suppress/{id}, GET .../push/status/{message_id} |
| Campaigns | POST /campaigns/{id}/trigger — starting one, not authoring it |
Dashboard only — an API key will not work: workflows, segments, message templates, schedules, campaign create/edit/delete, messaging channel setup, team and workspace management.
403 and the permission looks right, check this list first — the likeliest cause is that the route is a dashboard one and no key can reach it.Permissions
API keys have granular permissions that control access to different resources:
Permission Categories
These twelve strings are the complete set any route checks:
| Category | Permissions | Description |
|---|---|---|
| Events | events:read, events:write | Track user events, and read them back |
| People | people:read, people:write, people:identify, people:delete | Manage user profiles. people:identify is the narrow, app-safe one — see below |
| Campaigns | campaigns:trigger | Trigger webhook campaigns. There is no key permission for creating or editing campaigns — that is dashboard-only |
| Devices | devices:register, devices:read, devices:write | devices:register registers and unregisters only, and is safe to embed in an app; devices:read lists only; devices:write does both |
| Push | push:send, push:read | push:send sends push (single and batch), validates tokens, and suppresses or unsuppresses a device token (POST .../push/tokens/suppress, DELETE .../push/tokens/suppress/{id}) — the only way to block a token from delivery. push:read gates the working push-status lookup (GET .../push/status/{message_id}) plus the delivery-history and analytics routes that answer 501 today. Neither is grantable on a public key: sending renders a template against the recipient's stored record |
| Full Access | * | Wildcard granting access to all resources |
Thirteen other permission names (events:list, people:list,
campaigns:read, campaigns:write, analytics:read, analytics:write,
segments:read, segments:write, webhooks:read, webhooks:write,
workspace:read, workspace:write, admin) are accepted when a key is
created but gate no route — a key holding only them can reach nothing. Do not
grant them expecting access. admin is the one to watch: despite the name it
grants nothing at all, and it is not a synonym for *.
people:identify deserves its own note. It authorizes POST /identify and
nothing else — creating or updating one person's traits by an identifier you
already know. It is deliberately narrower than people:write, which also
carries /people CRUD and a 1000-operation batch route, and it is the only
people: permission a public key may hold.
Common Permission Sets
For integrations that only need to view data:
{
"permissions": [
"events:read",
"people:read"
]
}
These two cover person and event reads. An API key can hold other read
scopes too — add devices:read to list a person's registered devices, or
push:read to look up a push delivery status. What no key can reach is
campaign and analytics reads: those are dashboard-only surfaces.
For client applications that track user behavior:
{
"permissions": [
"events:write",
"people:write"
]
}
For backend services that need complete control:
{
"permissions": ["*"]
}
Or explicitly — all twelve enforced permissions:
{
"permissions": [
"events:read",
"events:write",
"people:read",
"people:write",
"people:identify",
"people:delete",
"devices:read",
"devices:write",
"devices:register",
"campaigns:trigger",
"push:send",
"push:read"
]
}
Creating API Keys
Open Dashboard
Log in to your Flameup Dashboard and navigate to Settings > API Keys.
Say where the key will be used
On my server — you pick the permissions.
In my app or website — there are no permissions to pick. Flameup applies the public-key set for you, because there is exactly one combination that is safe to ship and choosing it by hand is a chance to get it wrong.
Name it
A descriptive name (e.g. "Backend Server", "Android app"). This is what you will see in the list later when deciding whether a key can be revoked.
Copy Your Key
Copy the full API key immediately. For security, the full key is only shown once.
Creating API Keys via the API
Key creation is authenticated with a dashboard session token, not with an API key — a key cannot mint another key. Use this when provisioning programmatically.
Endpoint: POST /api/v1/workspaces/{workspace_id}/api-keys
curl -X POST "https://api.flameup.ai/api/v1/workspaces/{workspace_id}/api-keys" \
-H "Authorization: Bearer {dashboard_token}" \
-H "Content-Type: application/json" \
-d '{
"name": "Backend Server",
"permissions": ["events:write", "people:write", "people:read"],
"environment": "live"
}'
curl -X POST "https://api.flameup.ai/api/v1/workspaces/{workspace_id}/api-keys" \
-H "Authorization: Bearer {dashboard_token}" \
-H "Content-Type: application/json" \
-d '{
"name": "Android app",
"key_type": "public",
"permissions": ["events:write", "devices:register", "people:identify"],
"environment": "live"
}'
| Field | Required | Notes |
|---|---|---|
name | yes | Shown in the dashboard list |
permissions | yes | See Permissions. Validated against key_type |
key_type | no | secret (default) or public |
environment | no | live (default) or test. A label only — see below |
expires_at | no | ISO 8601. Must be in the future |
description | no | Free text, shown alongside the name |
ip_whitelist | no | Array of single IPs ("203.0.113.7") or CIDR blocks ("203.0.113.0/24"). See below |
TRUSTED_PROXIES configured, or
API_KEY_IP_ALLOWLIST_ENFORCE set). Treat the allowlist as defence in depth,
not as the sole control on a key. A malformed entry never matches — it fails
closed.Response (201):
{
"success": true,
"api_key": {
"id": "8f2a...",
"key_prefix": "pk_live_abc12345_abc123",
"key": "pk_live_abc12345_abc123.a1b2c3...",
"permissions": ["events:write", "devices:register", "people:identify"],
"key_type": "public"
},
"warning": "This is the only time the full API key will be shown. Please save it securely."
}
key is the value you authenticate with. It appears here and in a
refresh response — nowhere else. key_prefix is safe to log
and to display later.
400 with the allowed list in the message, before any key is created — so a rejected request never leaves a half-made key behind.environment does not isolate data. It is recorded on
the key and shown in the dashboard, but no query filters by it: a test-labelled
key writes to the same tables a live one reads (and both carry the same
ws_live_/pk_live_ prefix). To keep test traffic out of your real data, use a
separate workspace — that boundary is enforced on every read and write.Managing Keys
The same dashboard-token surface carries the full key lifecycle:
| Method | Endpoint | Does |
|---|---|---|
GET | /workspaces/{workspace_id}/api-keys | List keys (prefixes and metadata, never secrets) |
GET | /workspaces/{workspace_id}/api-keys/{key_id} | One key's metadata |
PUT | /workspaces/{workspace_id}/api-keys/{key_id} | Partial update — see below |
DELETE | /workspaces/{workspace_id}/api-keys/{key_id} | Revoke the key |
POST | /workspaces/{workspace_id}/api-keys/{key_id}/refresh | Rotate the secret — see Key Rotation |
GET | /workspaces/{workspace_id}/api-keys/{key_id}/usage | Usage statistics for the key |
PUT updates only the fields present in the body: name, description,
permissions, environment, expires_at, ip_whitelist. Permissions are
revalidated against the key's stored type, so a public key cannot be widened
after the fact. expires_at is three-state: omit it to leave the expiry
unchanged, send a timestamp to set it, or send an explicit null to clear
it and make the key non-expiring.
Security Features
Key Rotation
Regularly rotate your API keys for security. The refresh endpoint mints a
new secret for the same key: the key_prefix, id, and permissions are
unchanged, and the response carries the new full key — the only time it is
shown. The old secret stops working immediately.
curl -X POST "https://api.flameup.ai/api/v1/workspaces/{workspace_id}/api-keys/{key_id}/refresh" \
-H "Authorization: Bearer {dashboard_token}"
Error Responses
401 Unauthorized
Returned when no API key is provided or the key is invalid:
{
"error": "Unauthorized",
"message": "Invalid or expired API key",
"details": "Please provide a valid API key in the Authorization header: Bearer <your_api_key>"
}
403 Forbidden
Returned when the API key doesn't have the required permissions:
{
"error": "Forbidden",
"message": "Insufficient permissions"
}
429 Too Many Requests
Returned when rate limits are exceeded:
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again later.",
"scope": "ws-ingest",
"gate": "local"
}
See Rate Limits for what scope and gate mean.
Best Practices
Use a separate workspace per environment, with its own keys. Separate keys alone do not separate data — only the workspace boundary does. Never use production keys in development.