Scoped API Tokens: Least Privilege for Every Integration
Most teams end up with the same API token wired into half a dozen places: the CI pipeline, a monitoring dashboard, a backup script, a Terraform run, and a one-off cron job someone wrote last year. Every one of those tokens can do everything the platform allows, including deleting your production database. That is a lot of blast radius for a token whose only job is to read a metrics endpoint.
Scoped API tokens fix this. A token can now carry a set of scopes that restrict exactly which resource families it can touch and at what level. A monitoring dashboard gets a token that can only read services. A backup script gets a token that can only manage backups. The token that can delete a database stays in a human's password manager, where it belongs.
How Scopes Work
A scope is a string in the form family:level. There are five resource families and three levels.
The families map to the things a token might need to touch:
| Family | Covers |
|---|---|
services | Managed services: create, modify, scale, delete |
backups | Backups, restores, PITR ranges, restore jobs, backup schedules |
pipelines | Data pipelines (RAG, recommendation, event analytics) |
webhooks | Webhook endpoints and delivery configuration |
billing | Billing and usage data |
The levels are hierarchical. Each level includes everything below it:
| Level | Grants | Includes |
|---|---|---|
read | GET requests | read |
write | POST, PUT, PATCH (create and modify) | read, write |
admin | DELETE (destructive operations) | read, write, admin |
So services:admin covers services:write, which covers services:read. A token with backups:write can list and create backups but cannot delete them. A token with services:read can view services but cannot touch a single byte.
This is the whole model. Five families, three levels, and the same enforcement on every request.
Creating a Scoped Token
Tokens are created with POST /auth/tokens. Pass a name and the list of scopes you want.
curl -u $API_KEY:$API_SECRET -X POST \
https://api.foundrydb.com/auth/tokens \
-H "Content-Type: application/json" \
-d '{
"token_name": "ci-deploy",
"scopes": ["services:read", "services:write"]
}'
The response contains the token secret, shown exactly once, plus the stored token metadata.
{
"token": "fdb_live_8f2a9c1e4b7d6a3f0e5c8b1d4a7f2e9c",
"token_info": {
"id": "3c4d5e6f-7a8b-9c0d-1e2f-3a4b5c6d7e8f",
"token_name": "ci-deploy",
"scopes": ["services:read", "services:write"],
"created_at": "2026-06-07T09:12:44Z"
}
}
Copy the token value into your secret store now. The platform stores only a hash, so it cannot show it to you again. The token_info block, including the scopes, remains visible whenever you list your tokens.
Backward compatibility
If you omit the scopes field entirely, you get a full-access token. Internally it is stored as the explicit wildcard *, which matches every family and level. Every token you created before this feature shipped is treated the same way, so nothing breaks: existing automation keeps working with the exact permissions it had yesterday.
# No scopes: full-access token, identical behavior to before
curl -u $API_KEY:$API_SECRET -X POST \
https://api.foundrydb.com/auth/tokens \
-H "Content-Type: application/json" \
-d '{"token_name": "legacy-admin"}'
Scoping is opt-in. You tighten the tokens you want to tighten, on your own schedule.
Enforcement on Every Request
Scopes are checked on the request that needs them, not just at token creation. If a token tries to do something outside its scope, the request is rejected with a 403 and a body that names the exact scope it was missing.
Suppose your read-only monitoring token tries to scale a service:
curl -u fdb_live_readonly...: -X PATCH \
https://api.foundrydb.com/managed-services/$SERVICE_ID \
-H "Content-Type: application/json" \
-d '{"plan_name": "tier-4"}'
{
"error": "token does not have the required scope",
"required_scope": "services:write"
}
The error tells you precisely what the token would need. That makes it easy to debug a misconfigured integration: you do not have to guess which permission is missing, the response names it.
Backups Are Their Own Family
Backup operations that live under a service still belong to the backups family, not services. That includes listing and creating backups, triggering a restore, querying PITR ranges, inspecting restore jobs, and editing backup schedules.
This separation is deliberate. It lets you hand a backup tool a token that can manage backups end to end without giving it any power over the services themselves. The backup script can snapshot and restore, but it cannot delete the database it is protecting.
# Backup tooling: full control over backups, zero control over services
curl -u $API_KEY:$API_SECRET -X POST \
https://api.foundrydb.com/auth/tokens \
-H "Content-Type: application/json" \
-d '{
"token_name": "backup-runner",
"scopes": ["backups:admin"]
}'
With this token, POST /managed-services/$SERVICE_ID/backups works, but DELETE /managed-services/$SERVICE_ID returns 403 asking for services:admin.
What Scoped Tokens Cannot Do
Restricting families and levels is only half the story. A few capabilities are off limits to every scoped token regardless of its scopes, because they would defeat the point of scoping in the first place.
Scoped tokens cannot create new tokens. This is the escalation guard. If a services:read token could call POST /auth/tokens, it could simply mint itself a full-access token. So token creation requires a full-access token, full stop.
Scoped tokens cannot manage organizations, members, or credentials. Adding a member, changing roles, or rotating organization-level credentials all require a full-access token. These are administrative actions that should be performed by a human, not an integration.
Unknown scopes are rejected at creation. If you ask for a scope that does not exist (a typo like service:read or a made-up secrets:admin), the create call fails with a 400 instead of silently granting nothing or, worse, something unexpected.
{
"error": "unknown scope",
"invalid_scope": "service:read"
}
Dashboard sessions are unaffected. Scopes apply to API tokens. When you log into the web dashboard, your session carries your real role and permissions as before. Scoping does not change what humans can do in the UI.
Org-Level Oversight
Owners and admins can see and revoke every token issued by members of their organization. This is what makes scoped tokens usable for offboarding and incident response, not just convenience.
List every member token, with its scopes and when it was last used:
curl -u $API_KEY:$API_SECRET \
https://api.foundrydb.com/organizations/$ORG_ID/tokens \
| jq '.tokens[] | {token_name, scopes, last_used_at}'
{"token_name": "ci-deploy", "scopes": ["services:read", "services:write"], "last_used_at": "2026-06-07T08:55:10Z"}
{"token_name": "grafana-readonly", "scopes": ["services:read"], "last_used_at": "2026-06-07T09:10:02Z"}
{"token_name": "backup-runner", "scopes": ["backups:admin"], "last_used_at": "2026-06-06T02:00:11Z"}
The last_used_at timestamp is the fast way to spot a token that nobody uses anymore, or one that suddenly started being used from somewhere it should not be.
Revoking a token is a single call:
curl -u $API_KEY:$API_SECRET -X DELETE \
https://api.foundrydb.com/organizations/$ORG_ID/tokens/$TOKEN_ID
When a contractor leaves or you suspect a leak, you do not have to rotate one shared secret across every system. You revoke the specific token that was issued for that purpose and leave everything else running.
Dashboard Support
Everything above is available in the dashboard too. The token creation dialog has a picker for full-access versus scoped, and when you choose scoped you set the level per family. The token list shows each token's scopes as chips along with its last-used time, and the organization settings page has an API Tokens tab where owners and admins manage every member token.
A Token for Every Job
The point of scopes is that each integration gets exactly the access it needs and nothing more. A few patterns worth copying:
- Monitoring or dashboards:
services:read. It can render service status and metrics, and that is all it can ever do. - CI deploy pipeline:
services:readandservices:write. It can create and modify services for a deploy but cannot delete them, so a runaway job cannot wipe production. - Backup tooling:
backups:admin. Full backup and restore control, zero reach into the services. - Webhook manager: the
webhooksfamily only. It configures delivery endpoints without touching databases.
The full-access token stays with a person, used rarely, for the actions that genuinely require it.
What's Next
Scoped API tokens are available now for every FoundryDB organization. To start tightening your integrations:
- Review your existing tokens with
GET /organizations/{orgId}/tokensand find the ones that do not need full access - Create scoped replacements with
POST /auth/tokensand swap them in - Revoke the old full-access tokens once the scoped ones are in place
- Read the API reference for the full list of endpoints and the family each one belongs to
If you are not yet on FoundryDB, sign up and have your first database running in under five minutes.