API quickstart
Preview This page takes you from zero to a first request, then to a real async mutation — installing a plugin 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 and
wp.plugins:write to install a plugin. 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.NewClient(os.Getenv("FORGE_TOKEN"))
sites, err := client.Sites.List(ctx, &forge.SiteListParams{})if err != nil { log.Fatal(err)}for _, s := range sites.Data { fmt.Println(s.ID, s.Name, s.Runtime)}import Forge from "@managed.dev/sdk";
const client = new Forge({ token: process.env.FORGE_TOKEN });
const sites = await client.sites.list();for (const site of sites.data) { console.log(site.id, site.name, site.runtime);}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", "name": "acme-store", "runtime": "wordpress", "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 insufficient scope 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: install a plugin on an
environment. 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 install:
curl -X POST \ https://api.managed.dev/v1/sites/site_01J7ZC3W9Q8F2K7M3N0XB5R4D2/environments/env_01J8AA1B2C3D4E5F6G7H8J9K0L/components \ -H "Authorization: Bearer $FORGE_TOKEN" \ -H "Forge-Version: 2026-06-23" \ -H "Idempotency-Key: 6f1c8b2a-1d3e-4f5a-9b0c-2e7d8a1f3c4b" \ -H "Content-Type: application/json" \ -d '{ "kind": "plugin", "slug": "wordpress-seo", "source": "registry", "version": "latest", "activate": true }'job, err := client.Components.Install(ctx, forge.ComponentInstallParams{ SiteID: "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2", EnvironmentID: "env_01J8AA1B2C3D4E5F6G7H8J9K0L", Kind: "plugin", Slug: "wordpress-seo", Source: "registry", Version: "latest", Activate: true, IdempotencyKey: "6f1c8b2a-1d3e-4f5a-9b0c-2e7d8a1f3c4b",})if err != nil { log.Fatal(err)}fmt.Println("queued", job.ID)const job = await client.components.install( { siteId: "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2", environmentId: "env_01J8AA1B2C3D4E5F6G7H8J9K0L", kind: "plugin", slug: "wordpress-seo", source: "registry", version: "latest", activate: true, }, { idempotencyKey: "6f1c8b2a-1d3e-4f5a-9b0c-2e7d8a1f3c4b" },);
console.log("queued", job.id);mf components install \ --site site_01J7ZC3W9Q8F2K7M3N0XB5R4D2 \ --env env_01J8AA1B2C3D4E5F6G7H8J9K0L \ --kind plugin --slug wordpress-seo --activate --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": "component.install", "status": "queued", "progress": 0, "created_at": "2026-06-23T18:04:11.412Z", "resource": { "type": "component", "kind": "plugin", "slug": "wordpress-seo", "site_id": "site_01J7ZC3W9Q8F2K7M3N0XB5R4D2", "env_id": "env_01J8AA1B2C3D4E5F6G7H8J9K0L" }, "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 and Terraform.
curl https://api.managed.dev/v1/jobs/job_01J9XK4M2N7P0QR5S6T7U8V9W0 \ -H "Authorization: Bearer $FORGE_TOKEN"done, err := client.Jobs.Wait(ctx, "job_01J9XK4M2N7P0QR5S6T7U8V9W0")if err != nil { log.Fatal(err)}fmt.Println(done.Status) // "succeeded"const done = await client.jobs.wait("job_01J9XK4M2N7P0QR5S6T7U8V9W0");console.log(done.status); // "succeeded"mf jobs watch job_01J9XK4M2N7P0QR5S6T7U8V9W0{ "data": { "id": "job_01J9XK4M2N7P0QR5S6T7U8V9W0", "type": "component.install", "status": "succeeded", "progress": 100, "created_at": "2026-06-23T18:04:11.412Z", "result": { "kind": "plugin", "slug": "wordpress-seo", "version": "24.1", "active": true } }, "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.