Create an API key
Minting a key takes a minute and is where you decide everything it can do: its scopes, an optional resource constraint, and how long it lives. Get this right and the key is least-privilege by construction — it can touch exactly what you intended and nothing more.
What a key sees, and what it can do
Section titled “What a key sees, and what it can do”Before you mint anything, two behaviors decide what your key returns. Lead with these, because they explain responses that otherwise look surprising:
- Missing scope on a resource you can see →
403with the codescope.insufficient. The resource exists and you’re entitled to know it exists, but this key wasn’t granted the scope for this action. Add the scope (or use a key that has it). - Out of scope on a resource you can’t see →
404 not_found. If a key is pinned to one site, anything outside that pin doesn’t just refuse — it reports as not found. This existence hiding is deliberate: a narrowly-scoped key can’t enumerate what it has no business knowing about. The full rationale is in the security model.
The practical consequence: a 404 from a constrained key often means “outside this key’s
scope,” not “deleted.” Widen the constraint, or use a less-restricted key, to confirm.
Mint a key
Section titled “Mint a key”-
Open the keys page. In app.managed.dev, go to your account settings and open API keys, then Create key.
-
Name the key. Use a name that says where it runs —
ci-deploy-acme,terraform-prod,grafana-readonly. The name shows up in the audit log and on every last-used record, so future-you can tell which key did what. -
Set an expiry (optional, recommended). Pick how long the key lives — up to one year (365 days). Expiry is optional, but a shorter-lived key is safer; rotate before it lapses.
-
Pick scopes. Search and bulk-select the scopes the key needs —
sites:readandjobs:readfor a dashboard, adddeployments:writefor a deploy bot. The isolated scopes —credentials:read,credentials:write,exec:raw,keys:read,keys:write— never ride in via a wildcard and must be added one at a time, on purpose. Constrain the key (optional). Pin the key to a single site so it can never act outside that boundary — a CI key for one client’s site should be pinned to that site. You can also add an IP allowlist here. Team, project, and environment pin types are defined but not mintable yet Preview.
-
Confirm and copy the secret. The key is shown once. Copy it into your secret store now — you can’t retrieve it later, only roll or revoke and re-mint.
Create a key with POST /v1/api-keys. This requires the keys:write
scope — an isolated scope you grant one key at a time, so minting
keys is itself a privileged action. The plaintext secret is returned only in this
response.
curl -X POST https://api.managed.dev/v1/api-keys \ -H "Authorization: Bearer mfk_live_…" \ -H "Forge-Version: 2026-06-23" \ -H "Idempotency-Key: 6f1c-…" \ -H "Content-Type: application/json" \ -d '{ "name": "ci-deploy-acme", "scopes": ["sites:read", "deployments:write", "environments:write", "jobs:read", "observability:read"], "expires_in_days": 90, "resource": { "type": "site", "id": "site_01J7…" }, "ip_allowlist": ["203.0.113.7/32"] }'{ "data": { "id": "key_01JABC…", "name": "ci-deploy-acme", "prefix": "mfk_live", "last4": "x7Qd", "secret": "mfk_live_01JABC…_9aF2…", // shown once, never returned again "scopes": ["sites:read", "deployments:write", "environments:write", "jobs:read", "observability:read"], "resource": { "type": "site", "id": "site_01J7…" }, "expires_at": "2026-10-02T18:04:11Z", "created_at": "2026-07-04T18:04:11Z" }, "request_id": "req_01J9…"}scopes is the only required field. resource accepts type: "site" today (team,
project, and environment pins are defined but deferred), expires_in_days takes 1–365,
and ip_allowlist takes exact IPs and CIDRs. Listing and reading keys afterwards needs
keys:read (or keys:write) — and only ever returns metadata, never the secret.
mf keys create mints through the same endpoint and prints the secret exactly once.
mf keys create \ --name ci-deploy-acme \ --scopes sites:read,deployments:write,environments:write,jobs:read,observability:read \ --ttl 2160h \ --resource-type site --resource-id site_01J7… \ --ip-allowlist 203.0.113.7/32--ttl is rounded up to whole days. Also useful: mf login --mint --scopes … [--ttl …]
verifies the key you paste, mints a narrower key from it, and stores that one — so
the credential on your laptop is a down-scoped copy, not your broad original:
mf login --mint --scopes sites:read,jobs:read --ttl 24hRotate without a cutover
Section titled “Rotate without a cutover”When it’s time to rotate — before an expiry, after an offboarding, on any suspicion —
roll the key instead of revoking it cold. POST /v1/api-keys/{key_id}/roll (or
mf keys roll <id>) mints a new secret on the same key id, and the old secret
stays valid for 24 hours. Deploy the new secret everywhere the key runs, and the
old one dies on its own. The new secret, like the original, is shown exactly once.
For an immediate kill, DELETE /v1/api-keys/{key_id} (or mf keys revoke <id>)
revokes with no grace period.
Choosing scopes deliberately
Section titled “Choosing scopes deliberately”Start from the scope catalog and grant only what the job
needs — read is implied by write, so you never list both. Remember the down-scoping
rule: a key can never exceed the role of the principal
that minted it, no matter which scopes you check. And when you need to hand a narrow
bundle to a laptop or a script quickly, mf login --mint --scopes … is the shipped
shortcut — it derives a smaller key from the one you already hold. See
scopes for the grammar and the full intersection rule.