API keys — for CI, and for you
Anything that talks to Fadebox without a browser session authenticates with an API key
(Authorization: Bearer fbx_...). There are two kinds, and which you want depends on whether the
thing acting is a pipeline or a person.
- A service-account key is for CI. A service account is a user of origin
SERVICEwith roles like anyone else, but no password and no interactive sign-in; a project maintainer creates its keys on the project page (or via the API). Instances it deploys are owned by the account, not by whoever clicked the button. - A personal key is your own credential, for the CLI on your machine and for one-off scripts. You create it under Settings → API Keys, and it acts as you. See Your own keys.
Both are fbx_ tokens, both are shown exactly once, and both follow the same rule below.
The model
The account is the key's scope, the key is only the credential: a key acts with exactly the account's roles and project memberships, like a signed-in user.
- Creating a key from a project grants the account a project role there (default
deployer— the full instance lifecycle: create, deploy, stop, delete, status, logs). A pipeline that also writes the environment definition — pinning this build's image tags on it, say — needseditorinstead; one that only reads needsviewer. See who gets which role. - A maintainer may only issue or revoke keys for accounts whose entire authority they could have
granted a colleague — an account with a global role (say
template_adminfor a CI job that manages templates) or memberships in other projects is issued its keys by an admin. - Creating a key without naming an account uses a per-project default (
svc-<project>), auto-created on first use; naming an account shares it across keys and projects. - Instances created via a key are owned by the service account — every key of the account acts as the same creator, so a pipeline's cleanup job can stop what its deploy job started.
The token is shown exactly once at creation (only its hash is stored); revoke a key by deleting it, or disable the whole account by deactivating it.
Expiry
A key can be given a lifetime — 30 days, 90 days, a year — after which it stops authenticating. The key stays in the list, marked expired, so a pipeline that suddenly fails has a visible cause rather than a mystery.
A lifetime is a span, not a date on the calendar: a key made at 4pm for 30 days stops working at 4pm on the thirtieth day, wherever you and it happen to be. The list says how long each key has left — expires in about 1 month — with the exact moment on hover.
Expiry is optional, and CI keys default to not having one. A key sitting in a secret store that quietly lapses breaks a deploy for whoever happens to be on call, so committing to a rotation schedule should be a decision rather than a default. Personal keys default the other way, to 90 days — they live on laptops.
Your own keys
Settings → API Keys issues a key for your own account. It carries exactly what you carry: the same global roles, the same projects, the same everything. That is the whole model — there is no narrower key, and no way to make one do less than you can.
Three consequences worth knowing before you create one:
- Only you can issue one, and only you can revoke one. Not a project maintainer, not an admin. A key that acted as someone else would put their name on every request it made, so the route to issue one simply doesn't exist. What an admin can do is deactivate your account or force a password reset — either one refuses every key you hold, from the next request onwards, and both are reversible.
- A key cannot create another key. If you try, through either the personal or the project
route, you get a
403and a message saying to sign in. This is why a leaked token cannot quietly grow itself a replacement that survives revoking the original. - Group changes from your identity provider reach a key only after you next sign in. A key reads the roles stored in Fadebox, and those refresh at interactive sign-in. Anything changed in Fadebox itself — a revoked role, a deactivated account — applies on the very next request.
If you sign in through SSO you have no password, so a personal key is how you use the CLI at all.
Using the key
An AI agent is the third consumer, and the one that most wants a key of its own: point Claude Code or an IDE assistant at the MCP endpoint and it reads and operates your environments with exactly the authority the key's account has. Give each agent its own service account, and do not hand one a personal key.
The fadebox CLI is the intended client — it wraps the deploy loop, exits non-zero when a
deploy fails, and prints the instance URLs. The server hands the binary out itself, with no key
needed for that step, so a pipeline fetches it before its first command and always runs the
server's own version:
export FADEBOX_URL=https://fadebox.example.com
export FADEBOX_TOKEN=$CI_FADEBOX_KEY
export FADEBOX_PROJECT=demo FADEBOX_ENV=dev
curl -fsSL -o /usr/local/bin/fadebox "$FADEBOX_URL/cli/fadebox-linux-amd64"
chmod +x /usr/local/bin/fadebox
fadebox instance up pr-42 --wait
fadebox instance stop pr-42 --wait
The same thing over plain HTTP
The API is ordinary REST, so a pipeline that would rather not fetch a binary can do it with curl:
HOST=https://fadebox.example.com
AUTH="Authorization: Bearer $FADEBOX_API_KEY" # from your CI secret store
P=demo; E=dev; I=pr-42
# create + deploy an instance for this pipeline run
curl -sf -H "$AUTH" -H 'Content-Type: application/json' -d "{\"name\":\"$I\"}" \
"$HOST/api/projects/$P/environments/$E/instances"
curl -sf -X POST -H "$AUTH" "$HOST/api/projects/$P/environments/$E/instances/$I/deploy"
# wait until it is RUNNING (deploy is async — the call above returns 202 immediately)
until [ "$(curl -sf -H "$AUTH" "$HOST/api/projects/$P/environments/$E/instances/$I/status" \
| jq -r .status)" = RUNNING ]; do sleep 3; done
# logs of one service, then tear the instance down again
curl -sf -H "$AUTH" "$HOST/api/projects/$P/environments/$E/instances/$I/logs?service=app&tail=200"
curl -sf -X POST -H "$AUTH" "$HOST/api/projects/$P/environments/$E/instances/$I/stop"
curl -sf -X DELETE -H "$AUTH" "$HOST/api/projects/$P/environments/$E/instances/$I"
A robust script must also treat a final ERROR status as failure instead of looping forever —
which is exactly what the CLI's --wait is. GET .../status reports status plus containers,
keyed by service — .containers.web.urls[0] is the preview URL of the web service.
Either way, RUNNING means what the template's
healthchecks make it mean: a service without
one counts as ready the moment it starts, so give anything the pipeline will immediately talk to a
healthcheck, or the wait can end before the app answers.
Where a key can reach
A key is scoped by its account, so it sees what that account's roles and memberships reach: the
project list, the cross-project overview and the template catalog all narrow to them, and a 403
means a missing role, the same as for a person. See Users, groups and roles.
When a call is refused
A refusal carries a machine-readable code alongside the sentence, so a script can branch on the reason instead of matching on English:
HTTP 409
{
"error": "template_in_use",
"message": "Template 'postgres' is used by 3 environments — remove it from them before deleting it",
"args": { "template": "postgres", "count": 3 }
}
erroridentifies the rule that refused. Today:template_in_use,already_exists(with aresourcearg),environment_blueprint,environment_has_instances,instance_state,invalid_name(a name that has to become part of a container or ingress name is not a lowercase slug — withresource,patternandmaxLengthinargs),ingress_host_taken(another instance already serves an ingress hostname this deploy would claim — withhostand the holder'sheldBynamespace),environment_protected(the environment is protected and the key's account is not a maintainer of the project, so it may not change the definition), plus the licence refusals. More refusals gain codes over time.messageis always present and always readable, so a client that knows no codes loses nothing by ignoringerrorentirely.argscarries the values already in the message, split out — useful for building your own wording, and never worth parsing the sentence for.
Treat an unrecognised error as you would a bare status code: the message is the fallback, and
codes are added over time rather than renamed.