API keys
An API key is how a script, CI job, or integration authenticates to the managed.dev public API. Every key carries a set of scopes and, optionally, a resource constraint — together they decide exactly what the key can do, and the key can never do more than the person or team that minted it.
Credential families
Section titled “Credential families”The prefix on a credential tells you its type and its mode at a glance — and, just as
importantly, it tells secret scanners. The literal prefixes (mfk_live_, mfk_test_,
mfs_live_) are registered with leak-detection partners, so a key pasted into a public
repo is matched on the prefix and auto-revoked. See the
security model for how leak detection works.
| Family | Prefix | Acts as | Use case |
|---|---|---|---|
| Personal key | mfk_live_… / mfk_test_… |
a client (you) | scripts, dashboards, the mf CLI, personal automation |
| Service token Preview | mfs_live_… |
a team (machine principal) | CI/CD, Terraform, deploy bots — survives offboarding |
| OAuth token Preview | mfo_at_… / mfo_rt_… |
an integrator app, delegated by a client or team | marketplace integrations, third-party SaaS |
A full key looks like mfk_live_<public_id>_<secret> — the public id identifies the
key (it’s what you see in listings and the audit log); the trailing secret is the
credential itself.
A personal key dies with the client that owns it — revoke the person, and their
keys stop working. A service token is bound to a team, so it keeps running when an
individual leaves; that’s what you want for a deploy bot or a Terraform state. The API
recognizes mfs_live_ tokens today, but their full lifecycle — minting and managing
them from the dashboard — is still rolling out. OAuth is on the
roadmap for delegated, per-user access from third-party
apps — design it into your mental model now, but personal keys are what’s shipped.
Live vs test mode
Section titled “Live vs test mode”The _live_ and _test_ infix selects the mode the key operates in. A _test_ key
targets test-mode resources and is the safe default for local development and CI dry
runs; a _live_ key acts on your real sites and environments. Keep them separate the
way you’d keep production and sandbox credentials apart anywhere else.
How keys are stored and protected
Section titled “How keys are stored and protected”A key’s secret is shown once — at creation, or when a roll mints a new one — and never again. managed.dev stores only a SHA-256 hash of the secret — the plaintext is never written to disk, so even a database compromise can’t reveal an existing key. If you lose a key, you don’t recover it; you revoke it and mint a new one.
Every key carries:
- An optional expiry. Set
expires_in_days(1–365) when you mint and the key carries anexpires_at; leave it off and the API doesn’t force one. ThemfCLI applies a 90-day default when it mints, and that’s the habit worth keeping: a leaked key with a deadline is a much smaller problem. Rotate before expiry. - Rolling.
POST /v1/api-keys/{key_id}/rollmints a new secret on the same key id. The old secret stays valid for 24 hours, so you can swap the new one into your secret store without a hard cutover. - Revocation. Revoke a key instantly from the dashboard or the API. Because keys are opaque and checked against the database on every request, revocation takes effect immediately — there’s no stateless token to wait out. See the security model for why this beats a self-contained JWT.
- An optional IP allowlist. Constrain a key to a set of source addresses (exact IPs or CIDRs), so a CI key only works from your runners or your office egress.
- An optional resource pin. Constrain a key to a single site so it can never act outside that boundary. Site pins are what you can mint today; team, project, and environment pin types are defined but deferred Preview.
- Down-scope-only updates.
PATCH /v1/api-keys/{key_id}can rename a key or narrow its scopes to a subset of what it already holds — a broadening scope is refused. To widen a grant, mint a new key. - A mode and a prefix. As above — type, mode, and leak-detection identity, all encoded in the literal string.
Sending a key
Section titled “Sending a key”Pass the key as a bearer token on the Authorization header. That’s the only transport —
keys never go in query strings or request bodies.
curl https://api.managed.dev/v1/sites \ -H "Authorization: Bearer mfk_live_01JABC…_9aF2…" \ -H "Forge-Version: 2026-06-23"req, _ := http.NewRequest("GET", "https://api.managed.dev/v1/sites", nil)req.Header.Set("Authorization", "Bearer "+os.Getenv("FORGE_TOKEN"))req.Header.Set("Forge-Version", "2026-06-23")resp, err := http.DefaultClient.Do(req)const res = await fetch("https://api.managed.dev/v1/sites", { headers: { Authorization: `Bearer ${process.env.FORGE_TOKEN}`, "Forge-Version": "2026-06-23", },});export FORGE_TOKEN="mfk_live_…"mf sites listA request with a missing or malformed key returns 401 authentication; a valid key that
lacks the required scope for a resource you can see returns 403
with the code scope.insufficient. The full mapping is in the
errors reference.