API quickstart
This page takes you from zero to a first request, then to a real async mutation — triggering an on-demand backup and waiting for the job to finish. Pick your language with the tabs; the choice persists across the page.
1. Create a key
Section titled “1. Create a key”Mint a personal API key in the dashboard under Settings →
API keys, with the scopes this quickstart needs: sites:read to list sites,
backups:write to trigger a backup, and jobs:read to watch the job that tracks
it. The full secret — prefixed mfk_live_… — is
shown once at creation, so copy it then. See
creating keys for the walkthrough and
scopes for the catalog.
export FORGE_TOKEN="mfk_live_…"2. Make your first request
Section titled “2. Make your first request”List your sites. Every authenticated call carries the token as a bearer credential
and a pinned Forge-Version:
curl https://api.managed.dev/v1/sites \ -H "Authorization: Bearer $FORGE_TOKEN" \ -H "Forge-Version: 2026-06-23"client := forge.New(os.Getenv("FORGE_TOKEN"))
sites, err := client.Sites.List(ctx, nil)if err != nil { log.Fatal(err)}for _, s := range sites.Items { fmt.Println(s.ID, s.Domain, s.Status)}const mf = new Forge(process.env.FORGE_TOKEN!);
const page = await mf.sites.list();for (const s of page.items) { console.log(s.id, s.domain, s.status);}mf sites listThe response is the standard envelope —
data, a pagination block for collections, and a request_id you can quote when
asking for help:
{ "data": [ { "id": "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2", "slug": "acme-store", "domain": "acme-store.com", "profile": "bedrock", "php_version": "8.3", "runtime": "wordpress", "region": "us-east", "status": "running", "created_at": "2026-05-01T12:00:00Z" } ], "pagination": { "next_cursor": null, "has_more": false }, "request_id": "req_01J9F2K7M3N0XB5R4D2W9Q8F2K"}If you get a 401, the token is missing or wrong; a 403 with code
scope.insufficient means the key is valid but lacks sites:read. See
errors for the full taxonomy.
3. Run an async mutation
Section titled “3. Run an async mutation”Now do something that takes real work: trigger an on-demand backup. Mutations that
aren’t instantaneous return 202 Accepted with a
job and a Location header, rather than blocking the
connection.
Pass an Idempotency-Key so a retried request reuses
the same job instead of starting a second backup (the Go and TypeScript SDKs generate
one for you on every POST):
curl -X POST \ https://api.managed.dev/v1/sites/site_01J7ZC3W9Q8F2K7M3N0XB5R4D2/backup \ -H "Authorization: Bearer $FORGE_TOKEN" \ -H "Forge-Version: 2026-06-23" \ -H "Idempotency-Key: 6f1c8b2a-1d3e-4f5a-9b0c-2e7d8a1f3c4b"job, err := client.Backups.Create(ctx, "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2")if err != nil { log.Fatal(err)}fmt.Println("queued", job.ID)const job = await mf.backups.create("site_01J7ZC3W9Q8F2K7M3N0XB5R4D2");console.log("queued", job.id);mf backups create --site site_01J7ZC3W9Q8F2K7M3N0XB5R4D2 --waitThe 202 returns the job in the envelope. status is queued; the Location
header points at the job you’ll poll:
HTTP/1.1 202 AcceptedLocation: /v1/jobs/job_01J9XK4M2N7P0QR5S6T7U8V9W0{ "data": { "id": "job_01J9XK4M2N7P0QR5S6T7U8V9W0", "type": "backup.create", "status": "queued", "progress": 0, "created_at": "2026-06-23T18:04:11.412Z", "resource": { "type": "backup", "id": "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2", "site_id": "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2" }, "result": null, "error": null, "links": { "self": "/v1/jobs/job_01J9XK4M2N7P0QR5S6T7U8V9W0", "stream": "/v1/jobs/job_01J9XK4M2N7P0QR5S6T7U8V9W0/stream" } }, "request_id": "req_01J9F3A4B5C6D7E8F9G0H1J2K3"}4. Wait for it to finish
Section titled “4. Wait for it to finish”Poll the job until status is succeeded (or failed). The
async jobs page covers all three consumption paths — the
202 body, server-sent events, and ETag long-poll for
CI use.
curl https://api.managed.dev/v1/jobs/job_01J9XK4M2N7P0QR5S6T7U8V9W0 \ -H "Authorization: Bearer $FORGE_TOKEN"done, err := client.Jobs.WaitSuccess(ctx, "job_01J9XK4M2N7P0QR5S6T7U8V9W0", nil)if err != nil { var jobErr *forge.JobError if errors.As(err, &jobErr) { log.Fatalf("backup failed: %v", jobErr) // jobErr.Job holds the failed job } log.Fatal(err)}fmt.Println(done.Status) // "succeeded"try { const done = await mf.jobs.waitSuccess("job_01J9XK4M2N7P0QR5S6T7U8V9W0"); console.log(done.status); // "succeeded"} catch (err) { if (err instanceof JobError) console.error("backup failed:", err.job.error?.message); else throw err;}mf jobs watch job_01J9XK4M2N7P0QR5S6T7U8V9W0{ "data": { "id": "job_01J9XK4M2N7P0QR5S6T7U8V9W0", "type": "backup.create", "status": "succeeded", "progress": 1, "created_at": "2026-06-23T18:04:11.412Z", "resource": { "type": "backup", "id": "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2", "site_id": "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2" }, "result": { "snapshot_id": "snap_01J9YV5N3P8Q1R6S7T8U9V0W1X" }, "error": null, "links": { "self": "/v1/jobs/job_01J9XK4M2N7P0QR5S6T7U8V9W0", "stream": "/v1/jobs/job_01J9XK4M2N7P0QR5S6T7U8V9W0/stream" } }, "request_id": "req_01J9F4B5C6D7E8F9G0H1J2K3L4"}That’s the whole shape of the API: an authenticated request, a consistent envelope, and — for anything that takes real work — a job you can watch to completion.