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
POST /api/v1/spaces/:spaceId/jobs
Scope: jobs:create
Submit a new job to the space's dispatch queue.
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 |
timeoutSeconds | number | No | 300 | Timeout per attempt in seconds |
callbackUrl | string | No | null | Webhook URL to POST when job completes or goes dead |
callbackHeaders | object | No | null | Custom headers to include in the callback request (e.g., auth tokens) |
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" }
}'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" },
}),
});
const job = 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"},
},
)
job = 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"
}{ "error": "name is required" }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 |
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) |
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}'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 }),
});
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},
)
jobs = res.json()["jobs"]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. 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, "step": "encoding" },
"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" }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.
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" }Report Progress
POST /api/v1/jobs/:jobId/progress
Scope: jobs:progress
Send a progress update for a running job. The body is arbitrary JSON — use whatever structure makes sense for your use case. Progress events are broadcast to WebSocket subscribers.
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, "step": "encoding", "fps": 30}'await fetch(`${API}/jobs/${jobId}/progress`, {
method: "POST",
headers: { "x-api-key": key, "Content-Type": "application/json" },
body: JSON.stringify({ percent: 65, step: "encoding", fps: 30 }),
});Response:
{ "success": true }{ "error": "Job is not running" }Checkpoint Event
POST /api/v1/jobs/:jobId/event
Scope: jobs:event
Store a checkpoint or custom event on the job. Useful for resumable work — if the job fails and retries, your worker can read past checkpoints to resume from where it left off.
curl -X POST https://emit.run/api/v1/jobs/$JOB_ID/event \
-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}/event`, {
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" }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" }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 or dead (failed after all retries) — 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 |
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"
}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 |
POST /jobs/:id/ack | jobs:ack |
POST /jobs/:id/progress | jobs:progress |
POST /jobs/:id/event | jobs:event |
POST /jobs/:id/complete | jobs:complete |
POST /jobs/:id/fail | jobs:fail |
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:keepalive, and jobs:read:progress.