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.
Authorization
Section titled “Authorization”Reading jobs requires the jobs:read scope.
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.
Endpoints
Section titled “Endpoints”| 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 |
List parameters
Section titled “List parameters”| 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 |
The job envelope
Section titled “The job envelope”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:
{ "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.
Job types
Section titled “Job types”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 |
Job results
Section titled “Job results”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.
Watching a job — ETag long-poll
Section titled “Watching a job — ETag long-poll”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.
# first poll — note the returned ETagcurl -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 advancescurl -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"'etag := ""for { req, _ := http.NewRequest("GET", "https://api.managed.dev/v1/jobs/job_01J9SC", nil) req.Header.Set("Authorization", "Bearer mfk_live_9aF2…") req.Header.Set("Forge-Version", "2026-06-23") if etag != "" { req.Header.Set("If-None-Match", etag) } resp, _ := http.DefaultClient.Do(req) if resp.StatusCode == http.StatusNotModified { time.Sleep(2 * time.Second) continue // 304 — no change yet } etag = resp.Header.Get("ETag") // decode body; break when status is succeeded or failed}Watching a job — SSE stream
Section titled “Watching a job — SSE stream”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.
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"mf jobs watch job_01J9SCevent: jobdata: {"id":"job_01J9SC","type":"deployment.deploy","status":"running","progress":0.5}
event: jobdata: {"id":"job_01J9SC","type":"deployment.deploy","status":"succeeded","progress":1,"result":{"deployment_id":"dep_01JE7"}}
event: donedata: {"status":"succeeded"}