Skip to content

Async jobs

Preview Most mutations on managed.dev aren’t instantaneous — installing a plugin, cloning an environment, running a deploy. Rather than block the request, the API returns a job: a 202 Accepted, a Location header, and a tracking object you watch to completion. This async-native model is what lets CLI, Terraform, and your own automation all wait for the same work correctly.

A mutation that can’t complete synchronously responds with 202 Accepted, a Location header pointing at the job, and the job in the body:

202 from a creating POST
HTTP/1.1 202 Accepted
Location: /v1/jobs/job_01J9…
ETag: "w/qd-1"
Content-Type: application/json
{
"data": {
"id": "job_01J9…",
"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_01J7…", "env_id": "env_01J8…"
},
"result": null,
"error": null,
"links": { "self": "/v1/jobs/job_01J9…", "stream": "/v1/jobs/job_01J9…/stream" }
},
"request_id": "req_01J9…"
}

Every job, from every endpoint, has the same shape:

Field Description
id The job id (job_01J9…). Quote it to support; use it to poll or stream.
type What’s running, e.g. component.install, environment.clone, deployment.run.
status queuedrunningsucceeded | failed | canceled.
progress 0100 where the operation reports it; advisory, not every job is granular.
created_at When the job was accepted.
resource The thing being acted on — its type, id, and any context (site_id, env_id, …).
result Populated on succeeded with the outcome (the new resource, output, etc.). null until then.
error Populated on failed with the error object. null otherwise.
links.self The job’s own URL — GET it to poll.
links.stream The SSE endpoint for a live tail.

A terminal status (succeeded, failed, canceled) is final — the job stops changing once it reaches one.

Pick the path that fits your environment. All three watch the same job.

For fire-and-forget work, the job in the 202 body may be all you need — you have the id for later, and you can move on. Come back and GET it whenever you want the outcome.

For a CLI, a UI, or anything that wants real-time progress, stream the job over server-sent events. Events arrive as the status and progress change, including streamed command output where the job produces it:

GET /v1/jobs/{id}/stream — live tail
curl -N https://api.managed.dev/v1/jobs/job_01J9…/stream \
-H "Authorization: Bearer mfk_live_…" \
-H "Accept: text/event-stream" \
-H "Forge-Version: 2026-06-23"
The event stream
event: status
data: {"id":"job_01J9…","status":"running","progress":40}
event: log
data: {"line":"Installing wordpress-seo (latest)…"}
event: status
data: {"id":"job_01J9…","status":"succeeded","progress":100}

The mf jobs watch command and the dashboard both ride this stream.

For SSE-hostile environments — Terraform providers, CI runners behind proxies that buffer streams — GET the job with If-None-Match. The job returns an ETag; send it back and you get 304 Not Modified until something changes, then the new state:

GET /v1/jobs/{id} with If-None-Match — efficient poll
curl -i https://api.managed.dev/v1/jobs/job_01J9… \
-H "Authorization: Bearer mfk_live_…" \
-H 'If-None-Match: "w/qd-1"' \
-H "Forge-Version: 2026-06-23"

A 304 means “nothing new — keep waiting”; a 200 carries the updated job and a fresh ETag to poll against next. This is how the Terraform provider waits for a job without holding a stream open.

A worked example — install a plugin and tail it

Section titled “A worked example — install a plugin and tail it”
1. Start the install — returns a job
curl -i -X POST \
https://api.managed.dev/v1/sites/site_01J7…/environments/env_01J8…/components \
-H "Authorization: Bearer mfk_live_…" \
-H "Forge-Version: 2026-06-23" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "kind": "plugin", "slug": "wordpress-seo", "version": "latest", "activate": true }'
# → 202 Accepted, Location: /v1/jobs/job_01J9…
2. Tail it to completion over SSE
curl -N https://api.managed.dev/v1/jobs/job_01J9…/stream \
-H "Authorization: Bearer mfk_live_…" \
-H "Accept: text/event-stream" \
-H "Forge-Version: 2026-06-23"
# → status running → log lines → status succeeded

GET /v1/jobs lists jobs for the principal, filterable and cursor-paginated:

Recent failed jobs
curl "https://api.managed.dev/v1/jobs?status=failed&resource_type=deployment&limit=20" \
-H "Authorization: Bearer mfk_live_…" \
-H "Forge-Version: 2026-06-23"

Reading jobs requires the jobs:read scope. See the jobs API resource for the full parameter set.