Skip to content

Async jobs

Most mutations on managed.dev aren’t instantaneous — running a build, cloning an environment, creating a backup. 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 the CLI, the SDK, 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…
Content-Type: application/json
{
"data": {
"id": "job_01J9…",
"type": "environment.create",
"status": "queued",
"progress": 0,
"created_at": "2026-06-23T18:04:11.412Z",
"resource": {
"type": "environment",
"id": "env_01J8…",
"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 — a dotted public type like deployment.deploy, build.run, environment.push, backup.create, or malware.scan.
status queuedrunningsucceeded | failed. Exactly these four values.
progress 01. Coarse today (0 queued, 0.5 running, 1 terminal) until operations report per-step progress.
created_at When the job was accepted.
resource The thing being acted on: its type (e.g. deployment, build, environment, backup), its id, and the tenancy context (site_id, and env_id when environment-scoped).
result Populated on succeeded with the operation’s output ids — keys like artifact_id, deployment_id, snapshot_id, build_id, scan_id, url. 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) is final — the job stops changing once it reaches one. A cancelled operation surfaces as failed with an explanatory error.

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. The stream carries the job’s live output as it’s produced — build logs, command output — and closes with a terminal done event carrying the final status:

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
data: Building wordpress artifact from refs/heads/main…
data: Signed artifact art_01J9… (sha256:9f2c…)
event: done
data: succeeded

The done payload speaks the same public status set as GET /v1/jobssucceeded or failed. The mf jobs watch command and the dashboard both ride this stream.

For SSE-hostile environments — 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 the right path for automation that can’t hold a stream open.

A worked example — run a build and tail it

Section titled “A worked example — run a build and tail it”
1. Kick off the build — returns a job
curl -i -X POST \
https://api.managed.dev/v1/sites/site_01J7…/environments/env_01J8…/builds \
-H "Authorization: Bearer mfk_live_…" \
-H "Forge-Version: 2026-06-23" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "builder": "wordpress", "source_ref": "refs/heads/main" }'
# → 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"
# → build output → event: done, data: succeeded

GET /v1/jobs lists jobs for the principal, cursor-paginated and filterable by status, resource_type, and site_id:

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

Reading jobs requires the jobs:read scope. From the CLI, use mf jobs list [--site S] [--status ST] and mf jobs get <id>. See the jobs API resource for the full parameter set.