Skip to content

Jobs

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, backup
site_id string no narrow to one site’s jobs
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.5,
"created_at": "2026-06-24T01:02:00.110Z",
"resource": { "type": "deployment", "id": "dep_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 is exactly queued → running → succeeded | failed — a cancelled operation surfaces as failed. progress is coarse (0, 0.5, 1) until operations record per-step progress. 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 resource the job’s type names — the build, deployment, or scan id once recorded, else the environment id, else the site id.

type is a stable dotted string naming the operation:

Family Types
deployment deployment.deploy, deployment.deploy_artifact, deployment.promote, deployment.rollback, deployment.wasm_deploy, deployment.wasm_rollback
build build.run
site site.restart, site.delete, site.config_apply, site.ssh_keys_apply, site.plugin_update
environment environment.clone, environment.create, environment.destroy, environment.refresh, environment.reset, environment.config_apply, environment.push, environment.suspend, environment.resume
backup backup.create, backup.restore
security malware.scan

On success, result carries the operation’s output as a small whitelisted set of keys — never arbitrary internals. The keys you may see: artifact_id, build_id, snapshot_id, deployment_id, scan_id, release_id, digest, commit, ref, branch, url, env_id, key_id. A build job, for example, finishes with result.artifact_id — the input for creating a deployment.

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, pushes the job envelope on every state change, and closes with a terminal done event — 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","type":"deployment.deploy","status":"running","progress":0.5}
event: job
data: {"id":"job_01J9SC","type":"deployment.deploy","status":"succeeded","progress":1,"result":{"deployment_id":"dep_01JE7"}}
event: done
data: {"status":"succeeded"}