API keys
Preview This resource manages the keys that authenticate every other call. You can list, create, update, and revoke API keys programmatically, roll a key to a new secret with a grace window so nothing breaks mid-rotation, and read your current rate-limit budget. Creating a key reveals its secret exactly once — there’s no way to read it back later.
For what keys are, the credential families (mfk_live_…, mfs_live_…), and how to
mint your first one in the dashboard, see API keys and
creating keys. This page is the API for managing them
once you’re automating.
Authorization
Section titled “Authorization”Minting and rotating keys requires the keys:write scope.
There is no admin scope. Keys are forced to Role=client, so a key — even one minted
by an admin — can never act as platform admin. See the
security model.
Endpoints
Section titled “Endpoints”| Method + path | Scope | Does |
|---|---|---|
GET /v1/api-keys |
keys:write |
list your keys (metadata only — never the secret) |
POST /v1/api-keys |
keys:write |
create a key → reveals the secret once |
GET /v1/api-keys/{id} |
keys:write |
get one key’s metadata |
PATCH /v1/api-keys/{id} |
keys:write |
update name, scopes, TTL, or IP allowlist |
DELETE /v1/api-keys/{id} |
keys:write |
revoke a key immediately |
POST /v1/api-keys/{id}/roll |
keys:write |
rotate to a new secret with a grace window |
GET /v1/rate-limits |
any | introspect your current rate-limit budget |
Create parameters
Section titled “Create parameters”| Name | Type | Required | Description |
|---|---|---|---|
name |
string |
yes | a human label, e.g. ci-deploy-bot |
scopes |
string[] |
yes | the scopes to grant, or a preset name |
resource |
object |
no | pin the key to a team, project, site, or env |
expires_in |
string |
no | TTL, default 90d, max 1y |
ip_allowlist |
string[] |
no | CIDRs the key may be used from |
Worked example — create a key (reveal-once)
Section titled “Worked example — create a key (reveal-once)”The secret comes back once, in the create response, in the secret field. Store
it immediately; subsequent reads of the key return only its metadata.
curl -X POST https://api.managed.dev/v1/api-keys \ -H "Authorization: Bearer mfk_live_9aF2…" \ -H "Forge-Version: 2026-06-23" \ -H "Content-Type: application/json" \ -d '{ "name": "ci-deploy-bot", "scopes": ["sites:read", "deployments:write", "environments:write", "jobs:read"], "resource": { "site": "site_01J7Q2" }, "expires_in": "90d" }'body := strings.NewReader(`{ "name": "ci-deploy-bot", "scopes": ["sites:read","deployments:write","environments:write","jobs:read"], "resource": { "site": "site_01J7Q2" }, "expires_in": "90d"}`)req, _ := http.NewRequest("POST", "https://api.managed.dev/v1/api-keys", body)req.Header.Set("Authorization", "Bearer mfk_live_9aF2…")req.Header.Set("Forge-Version", "2026-06-23")req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) // read resp.data.secret now — it is shown onceconst res = await fetch("https://api.managed.dev/v1/api-keys", { method: "POST", headers: { Authorization: "Bearer mfk_live_9aF2…", "Forge-Version": "2026-06-23", "Content-Type": "application/json", }, body: JSON.stringify({ name: "ci-deploy-bot", scopes: ["sites:read", "deployments:write", "environments:write", "jobs:read"], resource: { site: "site_01J7Q2" }, expires_in: "90d", }),});const { data } = await res.json();// data.secret is only present on this create response — persist it nowmf keys create \ --name ci-deploy-bot \ --scopes sites:read,deployments:write,environments:write,jobs:read \ --site site_01J7Q2 \ --ttl 90d{ "data": { "id": "key_01JF8R", "name": "ci-deploy-bot", "secret": "mfk_live_b1c4d8e2f6a0…", // shown once — store it now, it is hashed at rest "prefix": "mfk_live_b1c4…", "scopes": ["sites:read", "deployments:write", "environments:write", "jobs:read"], "resource": { "site": "site_01J7Q2" }, "expires_at": "2026-09-22T01:10:00Z", "created_at": "2026-06-24T01:10:00Z" }, "request_id": "req_01JAC1"}Rolling a key
Section titled “Rolling a key”POST /v1/api-keys/{id}/roll issues a new secret for the same key id while
keeping the old secret valid for a grace window. Deploy the new secret to your
runners, confirm traffic has moved over, and the old one expires on its own — no
window where every caller is broken at once.
curl -X POST https://api.managed.dev/v1/api-keys/key_01JF8R/roll \ -H "Authorization: Bearer mfk_live_9aF2…" \ -H "Forge-Version: 2026-06-23" \ -H "Content-Type: application/json" \ -d '{ "grace": "24h" }'mf keys roll key_01JF8R --grace 24h{ "data": { "id": "key_01JF8R", "secret": "mfk_live_9d3a71c0b8f4…", // the new secret — store it now "prefix": "mfk_live_9d3a…", "previous_prefix": "mfk_live_b1c4…", "previous_expires_at": "2026-06-25T01:12:00Z" // old secret valid until here }, "request_id": "req_01JAC9"}- Roll the key and capture the new
secret. - Deploy the new secret to every caller.
- Confirm the old prefix has gone quiet in the audit log.
- Let
previous_expires_atlapse, orDELETEthe key to cut the old secret off now.
Introspecting rate limits
Section titled “Introspecting rate limits”GET /v1/rate-limits reports your current budget without spending a request against
a real endpoint — useful for a client that wants to back off before it’s throttled.
See rate limits for the headers on every response.
curl https://api.managed.dev/v1/rate-limits \ -H "Authorization: Bearer mfk_live_9aF2…" \ -H "Forge-Version: 2026-06-23"{ "data": { "buckets": [ { "kind": "read", "limit": 1000, "remaining": 991, "reset": 1782192060 }, { "kind": "write", "limit": 100, "remaining": 100, "reset": 1782192060 }, { "kind": "create", "limit": 5, "remaining": 5, "reset": 1782192060 } ] }, "request_id": "req_01JACF"}