emit.run

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:

FieldTypeRequiredDefaultDescription
namestringYesJob identifier (e.g., send-email, generate-report)
payloadobjectNonullArbitrary JSON passed to your worker
maxRetriesnumberNo3Max retry attempts on failure
timeoutSecondsnumberNo300Timeout per attempt in seconds
callbackUrlstringNonullWebhook URL to POST when job completes or goes dead
callbackHeadersobjectNonullCustom 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:

ParamRequiredDefaultDescription
statusNoFilter: pending, delivered, running, completed, dead
limitNo50Max results (max: 100)
offsetNo0Pagination 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:

FieldTypeRequiredDefaultDescription
countnumberNo1Number 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 statecompleted or dead (failed after all retries) — the system sends a POST request to that URL.

When callbacks fire

Terminal stateTrigger
completedWorker calls the complete endpoint
deadJob 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:

HeaderValue
Content-Typeapplication/json
User-Agentemit.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 typeMeaning
callback_sentRequest succeeded (any HTTP status). Event data includes url and statusCode.
callback_failedRequest 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

EndpointRequired scope
POST /spaces/:id/jobsjobs:create
GET /spaces/:id/jobsjobs:read
POST /spaces/:id/jobs/polljobs:poll
GET /jobs/:idjobs:read
POST /jobs/:id/ackjobs:ack
POST /jobs/:id/progressjobs:progress
POST /jobs/:id/eventjobs:event
POST /jobs/:id/completejobs:complete
POST /jobs/:id/failjobs:fail
POST /jobs/:id/keepalivejobs: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.

On this page