API reference
Jobs
Create, read, list, follow and cancel jobs over HTTP.
A job lives in a space, and every path here starts with it: /v1/spaces/{space}/jobs. See Jobs for what each field does in practice.
Create jobs
POST /v1/spaces/{space}/jobs · scope create
Send one job, an array of jobs, or {"jobs": [...]}. The space is created by its first job.
namestringrequiredWhat kind of work it is. Workers claim jobs by name. At most 256 bytes.
payloadJSONWhat the worker needs, as any JSON value. At most 4 MiB.
keystringAn idempotency key. A job with a key an earlier job in the space has isn't made again: the answer is the earlier job's id, with outcome DUPLICATE. A key holds while its job is unfinished, and for 10 minutes after it finishes.
delayMsintegerDon't hand the job out before this many milliseconds from now. Up to a year.
runAtMsintegerDon't hand the job out before this Unix time, in milliseconds. Up to a year from now.
maxAttemptsintegerdefault: the space's (4)Tries in all, counting the first. From 1 to 100.
timeoutMsintegerdefault: the space's (300000)The lease a worker gets on each attempt, unless the worker asks for another.
backoffMsintegerdefault: the space's (1000)The wait after the first failure.
backoffobjectHow the waits after that grow, over the space's policy field by field: kind (EXPONENTIAL, LINEAR or CONSTANT), maxMs (the longest wait, at most an hour), multiplierPercent (100 to 1000; 200 doubles), jitter (NONE, FULL or EQUAL).
callbackobjectA webhook sent when the job finishes: url, and optional headers, an object of header names and values. See Callbacks.
singletonstringOnly one unfinished job with this value can exist in the space. A second create answers DUPLICATE with the open job's id. Once it finishes, the next create makes a new job. Not with key, and not in a FIFO space.
workflowbooleanThe job is a workflow run.
curl -s "$EMIT_URL/v1/spaces/billing/jobs" \
-H "Authorization: Bearer $EMIT_TOKEN" \
-H "Content-Type: application/json" \
-d '[
{"name": "charge-invoice", "payload": {"invoiceId": "inv_1042"}, "key": "charge-inv_1042"},
{"name": "charge-invoice", "payload": {"invoiceId": "inv_1043"}, "key": "charge-inv_1043", "delayMs": 60000}
]'const created = await emit.jobs.createMany([
{ name: "charge-invoice", payload: { invoiceId: "inv_1042" }, key: "charge-inv_1042" },
{ name: "charge-invoice", payload: { invoiceId: "inv_1043" }, key: "charge-inv_1043", delayMs: 60_000 },
]);created, err := c.Jobs.CreateMany(ctx, []emit.NewJob{
{Name: "charge-invoice", Payload: map[string]string{"invoiceId": "inv_1042"}, Key: "charge-inv_1042"},
{Name: "charge-invoice", Payload: map[string]string{"invoiceId": "inv_1043"}, Key: "charge-inv_1043", DelayMs: 60_000},
}){
"created": [
{ "id": "01M3QSGN0G01ZVECFQ48X49EVM", "outcome": "NEW", "attempt": 0 },
{ "id": "01M3QSGN0G024F4C1PA48D5EVM", "outcome": "NEW", "attempt": 0 }
]
}One entry per job, in the order sent. outcome is NEW, or DUPLICATE when the key or singleton already made a job, whose id this is. A space at its in-flight limit refuses the whole request with 429: see Errors.
Get a job
GET /v1/spaces/{space}/jobs/{id} · scope read
curl -s "$EMIT_URL/v1/spaces/billing/jobs/01M3QSGN0G01ZVECFQ48X49EVM" -H "Authorization: Bearer $EMIT_TOKEN"idstringThe job's id: 26 characters.
spacestringnamestringpayloadJSONstatusstringPENDING, RUNNING, COMPLETED, FAILED or CANCELLED.
attemptintegerThe current or last attempt: 0 before the first claim.
maxAttemptsintegerresultJSONWhat the worker completed it with, or the error of the last failure. null until then.
progressobjectThe latest progress: percent, message, and stages, the same per named stage.
eventsJSON[]Every event the workers reported, in order.
createdAtMsstringWhen it was created.
finishedAtMsstringWhen it finished; "0" until then.
leaseUntilMsstringWhile it runs, when the worker's lease ends.
notBeforeMsstringWhile it waits for a time (a delay, or the wait before a retry), when that wait ends. "0" otherwise.
callbackobjectIts callback, if it has one. Header values are shown empty.
flowobjectFor a workflow run: what it's up to. See Workflow runs.
List jobs
GET /v1/spaces/{space}/jobs · scope read
Newest first. Each entry is a summary: id, name, status, attempt, maxAttempts, createdAtMs, finishedAtMs, leaseUntilMs, notBeforeMs and progress. Get a job for its payload and result.
namestringOnly jobs of this name.
statusstringOnly jobs in this status: PENDING, RUNNING, COMPLETED, FAILED or CANCELLED.
fromMsintegerOnly jobs created at or after this Unix time, in milliseconds.
toMsintegerOnly jobs created before this time.
limitintegerAt most this many; up to 1,000.
pagestringThe nextPage of the page before.
runsbooleanOnly workflow runs, at most 100 a page. Each unfinished one has run: its node in the run's tree, without children or notes. Needs emit 0.6.
curl -s "$EMIT_URL/v1/spaces/billing/jobs?name=charge-invoice&status=FAILED&limit=50" \
-H "Authorization: Bearer $EMIT_TOKEN"const page = await emit.jobs.list({ name: "charge-invoice", status: "failed", limit: 50 });page, err := c.Jobs.List(ctx, emit.ListJobs{Name: "charge-invoice", Status: emit.Failed, Limit: 50}){ "jobs": [{ "id": "01M3QSGN0103P7EGVBX9D5NTSN", "name": "charge-invoice", "status": "FAILED", "attempt": 5, "maxAttempts": 5, "createdAtMs": "1790726132737", "finishedAtMs": "1790726139553", "leaseUntilMs": "0", "notBeforeMs": "0" }], "nextPage": "" }A page can be shorter than limit, even empty, and still have a nextPage: keep asking until nextPage is empty. A narrow time range is the cheapest way to look through a large history.
Read a job's log lines
GET /v1/spaces/{space}/jobs/{id}/lines · scope read
minLevelstringOnly lines at this level or above: TRACE, DEBUG, INFO, WARN, ERROR or FATAL.
sourcestringOnly lines from this source. Notes have the source note.
afterstringThe cursor of the last line you read, to go on from there.
limitintegerAt most this many; up to 1,000.
curl -s "$EMIT_URL/v1/spaces/billing/jobs/01M3QSGN0103P7EGVBX9D5NTSN/lines?minLevel=WARN" \
-H "Authorization: Bearer $EMIT_TOKEN"{ "lines": [{ "cursor": "9.0", "attempt": 1, "receivedAtMs": "1790726139530", "line": { "level": "WARN", "message": "card issuer slow to answer", "source": "", "atMs": "0", "metadata": null } }] }Lines come in the order they were written, from every attempt.
Follow a job
GET /v1/spaces/{space}/jobs/{id}/events · scope read, or a follow token
A stream of the job's changes: server-sent events, or a WebSocket if the request asks to upgrade. Each message is one JSON event, with atMs and one of these:
| Event | When |
|---|---|
now |
First: the job as it is, as Get a job shows it. |
claimed |
A worker took it: id, attempt, name, leaseUntilMs. |
progress |
New progress. |
event |
A new event. |
lines |
New log lines: their count and cursor, and the lines themselves with ?lines=true. |
retrying |
An attempt failed and another is coming: attempt, nextAtMs, error. |
released |
A worker gave it back unfinished. |
finished |
Last: its status and result. The stream then ends. |
linesbooleantrue to send log lines themselves, not only that there are new ones.
treebooleanFor a workflow run: send the whole run as a tree on every change instead. See Read a run's tree.
tokenstringA follow token, for a browser with no API token.
curl -s -N "$EMIT_URL/v1/spaces/billing/jobs/01M3QSGN0103P7EGVBX9D5NTSN/events" -H "Authorization: Bearer $EMIT_TOKEN"data: {"atMs":"1790726139614","now":{"id":"01M3QSGN0103P7EGVBX9D5NTSN","status":"RUNNING",...}}
data: {"atMs":"1790726139702","progress":{"percent":50.0,"message":"charging","stages":{}}}
data: {"atMs":"1790726140031","finished":{"status":"COMPLETED","result":{"chargeId":"ch_3PQ"}}}When two changes land close together, a follower may see only the later one. Treat the stream as the job's latest state, not as an exact log.
Make a follow token
POST /v1/spaces/{space}/jobs/{id}:followToken · scope read
A token that lets a browser follow one job without an API token, for the time you ask.
expiresInMsintegerrequiredHow long the token works: from a second to a year.
curl -s -X POST "$EMIT_URL/v1/spaces/billing/jobs/01M3QSGN0103P7EGVBX9D5NTSN:followToken" \
-H "Authorization: Bearer $EMIT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"expiresInMs": 3600000}'
# {"token":"t=1790729747943,v1=09360a4d2d6ccf9a..."}Cancel jobs
POST /v1/spaces/{space}/jobs:cancel · scope admin
Send an array of ids, or {"ids": [...]}. Add "tree": true to cancel workflow runs with everything they started.
curl -s "$EMIT_URL/v1/spaces/billing/jobs:cancel" \
-H "Authorization: Bearer $EMIT_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '["01M3QSGN0G01ZVECFQ48X49EVM", "01M3QSGN0G024F4C1PA48D5EVM"]'const outcomes = await emit.jobs.cancel("01M3QSGN0G01ZVECFQ48X49EVM", "01M3QSGN0G024F4C1PA48D5EVM");outcomes, err := c.Jobs.Cancel(ctx, "01M3QSGN0G01ZVECFQ48X49EVM", "01M3QSGN0G024F4C1PA48D5EVM"){ "outcomes": [{ "status": "CANCELLED" }, { "rejection": { "reason": "ALREADY_FINISHED", "message": "" } }] }One outcome per id: the job's status now, or why it wasn't cancelled (NOT_FOUND or ALREADY_FINISHED).