Skip to content

Jobs

Preview Any mutation that isn’t instantaneous — a deploy, a build, a clone, a malware scan, a config change — returns 202 Accepted with a job. The jobs resource is the read side of all of them: one place to list jobs, check a job’s status, and wait for it to finish. If you’ve seen a 202 anywhere else in this API, this is how you find out what happened next.

For the bigger picture — why operations are async, and the three ways to consume a 202 — see async jobs. This page is the endpoint reference.

Reading jobs requires the jobs:read scope.

jobs:read

The jobs list is loaderless — there’s no resource in /v1/jobs to load, so tenancy is resolved in-handler from your principal. A key’s resource constraint is honored here: a key pinned to a site or team only ever lists jobs for resources that key may see. There’s no jobs:write — you don’t create jobs directly, you create them by calling the operation that returns one.

Method + path Returns
GET /v1/jobs a paginated list of your jobs
GET /v1/jobs/{jobID} one job’s current status (returns an ETag)
GET /v1/jobs/{jobID}/stream the job’s progress, live, over SSE
Name Type Required Description
status string no filter to queued, running, succeeded, or failed
resource_type string no filter to a kind, e.g. deployment, build, environment, site
limit integer no page size
cursor string no opaque pagination cursor

Every job — listed, polled, or streamed — has the same stable shape, so an SDK or a Terraform provider can wait on any operation with one code path:

A job
{
"data": {
"id": "job_01J9SC",
"type": "deployment.deploy",
"status": "running",
"progress": 0.6,
"created_at": "2026-06-24T01:02:00.110Z",
"resource": { "type": "deployment", "id": "env_01J8D9", "site_id": "site_01J7AA", "env_id": "env_01J8D9" },
"result": null,
"error": null,
"links": { "self": "/v1/jobs/job_01J9SC", "stream": "/v1/jobs/job_01J9SC/stream" }
},
"request_id": "req_01JAB2"
}

status walks queued → running → succeeded | failed. On success, result carries the operation’s output (e.g. the new release id); on failure, error carries the error object. resource always carries the owning site_id, adds env_id for environment-scoped operations, and sets id to the most specific subject — the environment when there is one, otherwise the site.

GET /v1/jobs/{jobID} returns an ETag. Send it back as If-None-Match and the server returns 304 Not Modified until the job actually changes, then 200 with the new state. This is the polling path for SSE-hostile environments — Terraform providers, CI runners behind proxies — and it’s cheap to loop on.

Poll until the job changes
# first poll — note the returned ETag
curl -i https://api.managed.dev/v1/jobs/job_01J9SC \
-H "Authorization: Bearer mfk_live_9aF2…" \
-H "Forge-Version: 2026-06-23"
# → ETag: "w/job-3"
# subsequent polls — 304 until the job advances
curl -i https://api.managed.dev/v1/jobs/job_01J9SC \
-H "Authorization: Bearer mfk_live_9aF2…" \
-H "Forge-Version: 2026-06-23" \
-H 'If-None-Match: "w/job-3"'

GET /v1/jobs/{jobID}/stream holds the connection open and pushes the job envelope on every state change — ideal for a CLI or UI that wants live progress. See the SSE contract.

Stream a job to completion
curl -N https://api.managed.dev/v1/jobs/job_01J9SC/stream \
-H "Authorization: Bearer mfk_live_9aF2…" \
-H "Forge-Version: 2026-06-23" \
-H "Accept: text/event-stream"
Stream frames
event: job
data: {"id":"job_01J9SC","status":"running","progress":0.6}
event: job
data: {"id":"job_01J9SC","status":"succeeded","progress":1,"result":{"release_id":"rel_01JE7"}}