Jobs API
Create, list, poll, and manage jobs
Jobs API
All job endpoints require an API key. Space-scoped endpoints live under /api/v1/spaces/:spaceId/jobs, and job-specific endpoints live under /api/v1/jobs/:jobId.
Create Job(s)
POST /api/v1/spaces/:spaceId/jobs
Scope: jobs:create
Submit one or more jobs to the space's dispatch queue.
You can send either:
- a single job object (backward compatible), or
- an array of job objects for bulk ingestion in a single request.
Request body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Job identifier (e.g., send-email, generate-report) |
payload | object | No | null | Arbitrary JSON passed to your worker |
maxRetries | number | No | 3 | Max retry attempts on failure (0–25) |
timeoutSeconds | number | No | 300 | Timeout per attempt in seconds (1–86400); used for delivery + execution |
scheduledFor | string | No | null | ISO 8601 timestamp; delays the job until this time (must be in the future) |
callbackUrl | string | No | null | Webhook URL to POST when job reaches a terminal state (completed, dead, killed) |
callbackHeaders | object | No | null | Custom headers to include in the callback request (string values only) |
curl -X POST https://emit.run/api/v1/spaces/$SPACE_ID/jobs \
-H "x-api-key: $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"name": "process-video",
"payload": { "url": "s3://bucket/video.mp4", "format": "720p" },
"maxRetries": 5,
"timeoutSeconds": 600,
"callbackUrl": "https://example.com/webhooks/jobs",
"callbackHeaders": { "Authorization": "Bearer your-secret" }
},
{
"name": "generate-thumbnail",
"payload": { "url": "s3://bucket/video.mp4", "size": "small" }
}
]'const res = await fetch(`${API}/spaces/${spaceId}/jobs`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify([
{
name: "process-video",
payload: { url: "s3://bucket/video.mp4", format: "720p" },
maxRetries: 5,
timeoutSeconds: 600,
callbackUrl: "https://example.com/webhooks/jobs",
callbackHeaders: { Authorization: "Bearer your-secret" },
},
{
name: "generate-thumbnail",
payload: { url: "s3://bucket/video.mp4", size: "small" },
},
]),
});
const jobs = await res.json();res = requests.post(
f"{API}/spaces/{space_id}/jobs",
headers={"x-api-key": key, "Content-Type": "application/json"},
json=[
{
"name": "process-video",
"payload": {"url": "s3://bucket/video.mp4", "format": "720p"},
"maxRetries": 5,
"timeoutSeconds": 600,
"callbackUrl": "https://example.com/webhooks/jobs",
"callbackHeaders": {"Authorization": "Bearer your-secret"},
},
{
"name": "generate-thumbnail",
"payload": {"url": "s3://bucket/video.mp4", "size": "small"},
},
],
)
jobs = res.json()Response:
{
"id": "01JLQX...",
"spaceId": "01JLQ...",
"name": "process-video",
"payload": { "url": "s3://bucket/video.mp4", "format": "720p" },
"status": "pending",
"maxRetries": 5,
"attemptNumber": 0,
"timeoutSeconds": 600,
"callbackUrl": "https://example.com/webhooks/jobs",
"callbackHeaders": { "Authorization": "Bearer your-secret" },
"deliveredAt": null,
"startedAt": null,
"completedAt": null,
"result": null,
"createdAt": "2025-02-24T10:30:00.000Z",
"updatedAt": "2025-02-24T10:30:00.000Z"
}[
{
"id": "01JLQX...",
"spaceId": "01JLQ...",
"name": "process-video",
"payload": { "url": "s3://bucket/video.mp4", "format": "720p" },
"status": "pending"
},
{
"id": "01JLQY...",
"spaceId": "01JLQ...",
"name": "generate-thumbnail",
"payload": { "url": "s3://bucket/video.mp4", "size": "small" },
"status": "pending"
}
]{ "error": "jobs[0].name is required" }Scheduled (delayed) jobs
Set scheduledFor to an ISO 8601 timestamp in the future to delay a job. The job enters the scheduled state and is not available for polling until the scheduled time. When the time arrives, it automatically transitions to pending and enters the dispatch queue like any other job.
{
"name": "send-report",
"payload": { "reportId": "abc123" },
"scheduledFor": "2025-03-01T09:00:00.000Z"
}Scheduled jobs can be killed before they run. If scheduledFor is omitted or in the past, the job is created as pending immediately.
List Jobs
GET /api/v1/spaces/:spaceId/jobs
Scope: jobs:read
Query params:
| Param | Required | Default | Description |
|---|---|---|---|
status | No | — | Filter: pending, delivered, running, completed, dead, killed |
limit | No | 50 | Max results (max: 100) |
offset | No | 0 | Pagination offset |
# List all running jobs
curl "https://emit.run/api/v1/spaces/$SPACE_ID/jobs?status=running&limit=20" \
-H "x-api-key: $EMIT_KEY"const res = await fetch(
`${API}/spaces/${spaceId}/jobs?status=running&limit=20`,
{ headers: { "x-api-key": key } }
);
const jobs = await res.json();res = requests.get(
f"{API}/spaces/{space_id}/jobs",
headers={"x-api-key": key},
params={"status": "running", "limit": 20},
)
jobs = res.json()Response:
[
{
"id": "01JLQX...",
"spaceId": "01JLQ...",
"name": "process-video",
"payload": { "url": "s3://bucket/video.mp4", "format": "720p" },
"status": "running",
"maxRetries": 5,
"attemptNumber": 1,
"timeoutSeconds": 600,
"callbackUrl": "https://example.com/webhooks/jobs",
"callbackHeaders": { "Authorization": "Bearer your-secret" },
"deliveredAt": "2025-02-24T10:31:00.000Z",
"startedAt": "2025-02-24T10:31:05.000Z",
"completedAt": null,
"result": null,
"createdAt": "2025-02-24T10:30:00.000Z",
"updatedAt": "2025-02-24T10:31:05.000Z"
}
]Ordered by creation time (newest first). Returns an empty array if no jobs match.
Poll for Jobs
POST /api/v1/spaces/:spaceId/jobs/poll
Scope: jobs:poll
Claim pending jobs from the dispatch queue. Jobs are atomically dequeued — no two workers will receive the same job.
Request body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
count | number | No | 1 | Number of jobs to claim (max: 10) |
type | string | string[] | No | — | Preferred job-type filter. Pass one type or an array of types. |
types | string[] | No | — | Alias for type (array form). |
name | string | string[] | No | — | Backward-compatible alias for type. |
names | string[] | No | — | Alias for name (array form). |
curl -X POST https://emit.run/api/v1/spaces/$SPACE_ID/jobs/poll \
-H "x-api-key: $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"count": 5, "type": ["process-video", "generate-thumbnail"]}'const res = await fetch(`${API}/spaces/${spaceId}/jobs/poll`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify({ count: 5, type: ["process-video", "generate-thumbnail"] }),
});
const { jobs } = await res.json();res = requests.post(
f"{API}/spaces/{space_id}/jobs/poll",
headers={"x-api-key": key, "Content-Type": "application/json"},
json={"count": 5, "type": ["process-video", "generate-thumbnail"]},
)
jobs = res.json()["jobs"]If you send more than one filter field, precedence is types -> type -> names -> name.
Response:
{
"jobs": [
{
"id": "01JLQX...",
"name": "process-video",
"payload": { "url": "s3://bucket/video.mp4", "format": "720p" },
"timeoutSeconds": 600,
"maxRetries": 5,
"attemptNumber": 0,
"createdAt": "2025-02-24T10:30:00.000Z"
}
]
}Returns {"jobs":[]} if no jobs are available (or if none match your type/name filter). Your worker should back off and retry.
Get Job
GET /api/v1/jobs/:jobId
Scope: jobs:read
Fetch a job's full details including its event history.
curl https://emit.run/api/v1/jobs/$JOB_ID \
-H "x-api-key: $EMIT_KEY"const res = await fetch(`${API}/jobs/${jobId}`, {
headers: { "x-api-key": key },
});
const job = await res.json();Response:
{
"id": "01JLQX...",
"spaceId": "01JLQ...",
"name": "process-video",
"payload": { "url": "s3://bucket/video.mp4", "format": "720p" },
"status": "completed",
"maxRetries": 5,
"attemptNumber": 0,
"timeoutSeconds": 600,
"callbackUrl": "https://example.com/webhooks/jobs",
"callbackHeaders": { "Authorization": "Bearer your-secret" },
"deliveredAt": "2025-02-24T10:31:00.000Z",
"startedAt": "2025-02-24T10:31:05.000Z",
"completedAt": "2025-02-24T10:34:20.000Z",
"result": { "output_url": "s3://bucket/output.mp4", "duration_ms": 12340 },
"createdAt": "2025-02-24T10:30:00.000Z",
"updatedAt": "2025-02-24T10:34:20.000Z",
"events": [
{
"id": "01JLQY...",
"jobId": "01JLQX...",
"spaceId": "01JLQ...",
"type": "created",
"data": null,
"createdAt": "2025-02-24T10:30:00.000Z"
},
{
"id": "01JLQZ...",
"jobId": "01JLQX...",
"spaceId": "01JLQ...",
"type": "progress",
"data": { "percent": 65, "message": "Encoding video" },
"createdAt": "2025-02-24T10:32:00.000Z"
},
{
"id": "01JLR0...",
"jobId": "01JLQX...",
"spaceId": "01JLQ...",
"type": "completed",
"data": { "output_url": "s3://bucket/output.mp4", "duration_ms": 12340 },
"createdAt": "2025-02-24T10:34:20.000Z"
}
]
}{ "error": "Job not found" }Get Job Progress
GET /api/v1/jobs/:jobId/progress
Scope: jobs:read:progress or jobs:read
Return the most recent progress update (if any). This endpoint only exposes progress data.
curl https://emit.run/api/v1/jobs/$JOB_ID/progress \
-H "x-api-key: $EMIT_KEY"const res = await fetch(`${API}/jobs/${jobId}/progress`, {
headers: { "x-api-key": key },
});
const progress = await res.json();Response:
{
"jobId": "01JLQX...",
"progress": { "percent": 65, "message": "Encoding video" },
"timestamp": "2025-02-24T10:32:00.000Z"
}{ "error": "Job not found" }If no progress has been reported yet, progress and timestamp are null.
Acknowledge Job
POST /api/v1/jobs/:jobId/ack
Scope: jobs:ack
Tell the system your worker has received the job and is starting work. This moves the job from delivered to running and starts the execution timeout timer. The delivery timeout also uses timeoutSeconds until the job is acked.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/ack \
-H "x-api-key: $EMIT_KEY"await fetch(`${API}/jobs/${jobId}/ack`, {
method: "POST",
headers: { "x-api-key": key },
});Response:
{ "success": true }{ "error": "Job is not in a valid state for ack" }Worker Stop Signal
If a job is force-stopped via POST /jobs/:id/kill, worker mutation endpoints (progress, checkpoint, event, keepalive) return:
{
"error": "Job has been killed",
"code": "JOB_KILLED",
"jobStatus": "killed"
}The HTTP status is 409 in that case. Workers should treat this as an immediate stop signal for that job.
Report Progress
POST /api/v1/jobs/:jobId/progress
Scope: jobs:progress
Send a progress update for a running job. The body is strict and must only include:
percent— number from0to100message— non-empty user-facing string (e.g.Downloading video,Encoding,Uploading)
Progress events are broadcast to WebSocket subscribers and reset the execution timeout window.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/progress \
-H "x-api-key: $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"percent": 65, "message": "Encoding video"}'await fetch(`${API}/jobs/${jobId}/progress`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify({ percent: 65, message: "Encoding video" }),
});Response:
{ "success": true }{ "error": "Invalid progress payload. Only percent and message are allowed." }Checkpoint
POST /api/v1/jobs/:jobId/checkpoint
Scope: jobs:event
Store checkpoint data for resumable work. If the job fails and retries, your worker can read past checkpoints to resume from where it left off. Checkpoint events reset the execution timeout window.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/checkpoint \
-H "x-api-key: $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"checkpoint": "processed_frames", "count": 1500, "last_offset": 45000}'await fetch(`${API}/jobs/${jobId}/checkpoint`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify({
checkpoint: "processed_frames",
count: 1500,
last_offset: 45000,
}),
});Response:
{ "success": true }{ "error": "Job is not running" }Event
POST /api/v1/jobs/:jobId/event
Scope: jobs:event
Store an arbitrary debug/admin event payload on the job. This endpoint does not change job state; it only records and broadcasts the event. Custom events reset the execution timeout window.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/event \
-H "x-api-key: $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"phase":"encoder","host":"worker-3","note":"switched bitrate ladder"}'await fetch(`${API}/jobs/${jobId}/event`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify({
phase: "encoder",
host: "worker-3",
note: "switched bitrate ladder",
}),
});Response:
{ "success": true }{ "error": "Job is not running" }Complete Job
POST /api/v1/jobs/:jobId/complete
Scope: jobs:complete
Mark the job as successfully completed. Optionally include a result payload.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/complete \
-H "x-api-key: $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"result": {"output_url": "s3://bucket/output.mp4", "duration_ms": 12340}}'await fetch(`${API}/jobs/${jobId}/complete`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify({
result: { output_url: "s3://bucket/output.mp4", duration_ms: 12340 },
}),
});Response:
{ "success": true }{ "error": "Job is not running" }Fail Job
POST /api/v1/jobs/:jobId/fail
Scope: jobs:fail
Mark the job as failed. If the job has retries remaining, it will be re-queued as pending. Include an error message or object for debugging.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/fail \
-H "x-api-key: $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"error": "FFmpeg exited with code 1: out of memory"}'await fetch(`${API}/jobs/${jobId}/fail`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify({
error: "FFmpeg exited with code 1: out of memory",
}),
});Response:
{ "success": true }{ "error": "Job is not running" }Kill Job
POST /api/v1/jobs/:jobId/kill
Scope: jobs:kill
Force-stop a job and mark it as killed. This is useful for operator intervention, cancellation flows, or control-plane shutdowns.
If the job is already killed, this endpoint is idempotent and returns success.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/kill \
-H "x-api-key: $EMIT_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "manual stop"}'await fetch(`${API}/jobs/${jobId}/kill`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify({ reason: "manual stop" }),
});Response:
{ "success": true }{ "error": "Job not found", "code": "JOB_NOT_FOUND" }Keepalive
POST /api/v1/jobs/:jobId/keepalive
Scope: jobs:keepalive
Extend the job's timeout. Call this periodically for long-running jobs to prevent the system from marking them as timed out. Each call resets the timeout window.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/keepalive \
-H "x-api-key: $EMIT_KEY"// Send keepalive every 60 seconds for a long job
const interval = setInterval(async () => {
await fetch(`${API}/jobs/${jobId}/keepalive`, {
method: "POST",
headers: { "x-api-key": key },
});
}, 60_000);
try {
await doLongRunningWork();
await fetch(`${API}/jobs/${jobId}/complete`, { method: "POST", headers });
} finally {
clearInterval(interval);
}Response:
{ "success": true }{ "error": "Job is not running" }Callbacks (Webhooks)
Jobs can optionally specify a callbackUrl when created. When the job reaches a terminal state — completed, dead (failed after all retries), or killed (force-stopped) — the system sends a POST request to that URL.
When callbacks fire
| Terminal state | Trigger |
|---|---|
completed | Worker calls the complete endpoint |
dead | Job fails and has no retries remaining |
killed | Job is force-stopped via the kill endpoint |
Callbacks do not fire on intermediate failures that will be retried.
Callback payload
The POST body is JSON with this shape:
{
"jobId": "01JLQX...",
"spaceId": "01JLQ...",
"name": "process-video",
"status": "completed",
"attemptNumber": 0,
"result": { "output_url": "s3://bucket/output.mp4" },
"timestamp": "2025-02-24T10:35:00.000Z"
}For dead jobs, result contains the final error:
{
"jobId": "01JLQX...",
"spaceId": "01JLQ...",
"name": "process-video",
"status": "dead",
"attemptNumber": 3,
"result": { "error": "FFmpeg exited with code 1: out of memory" },
"timestamp": "2025-02-24T10:42:00.000Z"
}For killed jobs, result includes the kill metadata:
{
"jobId": "01JLQX...",
"spaceId": "01JLQ...",
"name": "process-video",
"status": "killed",
"attemptNumber": 1,
"result": { "reason": "manual stop" },
"timestamp": "2025-02-24T10:36:00.000Z"
}Request headers
Every callback request includes:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | emit.run/1.0 |
Any headers you set in callbackHeaders at job creation time are merged in, so you can include auth tokens or signing secrets:
{
"callbackUrl": "https://emit.run/webhooks/jobs",
"callbackHeaders": {
"Authorization": "Bearer whsec_abc123"
}
}Delivery tracking
Callback delivery is recorded as a job event:
| Event type | Meaning |
|---|---|
callback_sent | Request succeeded (any HTTP status). Event data includes url and statusCode. |
callback_failed | Request could not be sent (network error, DNS failure, etc.). Event data includes url and error. |
You can inspect these events via GET /api/v1/jobs/:jobId in the events array.
Scope Quick Reference
| Endpoint | Required scope |
|---|---|
POST /spaces/:id/jobs | jobs:create |
GET /spaces/:id/jobs | jobs:read |
POST /spaces/:id/jobs/poll | jobs:poll |
GET /jobs/:id | jobs:read |
GET /jobs/:id/progress | jobs:read:progress or jobs:read |
POST /jobs/:id/ack | jobs:ack |
POST /jobs/:id/progress | jobs:progress |
POST /jobs/:id/checkpoint | jobs:event |
POST /jobs/:id/event | jobs:event |
POST /jobs/:id/complete | jobs:complete |
POST /jobs/:id/fail | jobs:fail |
POST /jobs/:id/kill | jobs:kill |
POST /jobs/:id/keepalive | jobs:keepalive |
A token with jobs:worker covers all worker scopes: jobs:read, jobs:poll, jobs:ack, jobs:progress, jobs:event, jobs:complete, jobs:fail, jobs:kill, jobs:keepalive, and jobs:read:progress.