emit.run

WebSockets

Real-time job and space event streams

WebSockets

Subscribe to real-time events for a single job or an entire space. Events are pushed as JSON messages over a persistent WebSocket connection.

Auth: API key via ?token= query param — required on all endpoints.

Required scopes:

EndpointMinimum scope
GET /api/v1/realtime/:jobIdjobs:read:progress
GET /api/v1/realtime/spaces/:spaceIdjobs:read

Scope-Based Data Filtering

Your token's scope controls what data is included in events. This lets you safely hand tokens to end users without exposing job internals.

ScopeEvents delivered
jobs:read:progressinit (last known progress, if any), progress, and completed (without data)
jobs:readFull event stream with all payloads

With jobs:read:progress, clients receive progress updates plus a completion signal. Result/error payloads and all other event types are withheld. Use jobs:read if you need full completion payloads, failure details, or kill events.

Space scoping is enforced: a space-scoped token can only connect to its own space's stream and to jobs within that space. Connecting to a job in a different space returns 403.

Client-Facing Apps: Use the Public Token

Every space is automatically created with a public token — a pre-generated API key with jobs:read:progress scope. This is the recommended default for client-facing use cases: progress bars and live progress updates in your UI.

The public token is safe to embed in frontend code because it:

  • Can only subscribe to individual job streams — not the space-wide feed
  • Delivers progress updates and a completion signal (no result/error payloads)
  • Cannot create, poll, ack, complete, or list jobs in any way

Find it in the dashboard under TokensPublic read-only (progress).

// Backend: create a job, return the job ID to your client
const res = await fetch(`${API}/spaces/${spaceId}/jobs`, {
  method: "POST",
  headers: { "x-api-key": PRODUCER_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ name: "generate-report", payload: { reportId: 42 } }),
});
const job = await res.json();

// Frontend: connect using the space public token — no per-request token generation needed
const ws = new WebSocket(
  `wss://emit.run/api/v1/realtime/${job.id}?token=${SPACE_PUBLIC_TOKEN}`
);

ws.onmessage = (e) => {
  const event = JSON.parse(e.data);
  if (event.type === "init" && event.progress) {
    renderProgressBar(event.progress.percent);
  } else if (event.type === "progress") {
    renderProgressBar(event.data.percent); // full progress data included
  }
};

For failure or kill updates, use webhooks or have your backend send a final status update.


Job Stream

GET /api/v1/realtime/:jobId

Connect to a single job's event stream. You receive an init event immediately on connect — full status with jobs:read, or last known progress (if any) with jobs:read:progress — then live events as they occur.

ParamRequiredDescription
tokenYesAPI key with jobs:read:progress or higher.
const ws = new WebSocket(
  `wss://emit.run/api/v1/realtime/${jobId}?token=${apiKey}`
);

ws.onmessage = (e) => {
  const event = JSON.parse(e.data);

  switch (event.type) {
    case "init":
      if (event.progress) {
        console.log("Last progress:", event.progress);
      } else {
        console.log("Current status:", event.status);
      }
      break;
    case "progress":
      console.log("Progress:", event.data); // e.g. { percent: 65, message: "Encoding video" }
      break;
    case "completed":
      console.log("Done:", event.data); // full payload only with jobs:read
      ws.close();
      break;
    case "dead":
      console.error("Failed:", event.data); // full payload only with jobs:read
      ws.close();
      break;
    case "killed":
      console.warn("Stopped:", event.data); // full payload only with jobs:read
      ws.close();
      break;
    case "retried":
      console.log("Retrying, attempt:", event.data?.attemptNumber); // jobs:read only
      break;
  }
};
import asyncio
import json
import websockets

async def watch_job(job_id: str, api_key: str):
    uri = f"wss://emit.run/api/v1/realtime/{job_id}?token={api_key}"

    async with websockets.connect(uri) as ws:
        async for message in ws:
            event = json.loads(message)

            match event["type"]:
                case "init":
                    if event.get("progress"):
                        print("Last progress:", event["progress"])
                    else:
                        print("Current status:", event["status"])
                case "progress":
                    print("Progress:", event.get("data"))
                case "completed":
                    print("Done:", event.get("data"))  # jobs:read only
                    break
                case "dead":
                    print("Failed:", event.get("data"))  # jobs:read only
                    break
                case "killed":
                    print("Stopped:", event.get("data"))  # jobs:read only
                    break
                case "retried":
                    print("Retrying, attempt:", event.get("data", {}).get("attemptNumber"))  # jobs:read only

asyncio.run(watch_job("01JLQX...", "emit_YOUR_KEY"))
# websocat is a curl-like tool for WebSockets
websocat "wss://emit.run/api/v1/realtime/$JOB_ID?token=$EMIT_KEY"

Event reference

Full-scope events (jobs:read) share this shape:

{
  "jobId": "01JLQX...",
  "jobName": "process-video",
  "type": "progress",
  "status": "running",
  "data": { "percent": 65, "message": "Encoding video" },
  "timestamp": "2025-02-24T10:32:00.000Z"
}

Progress-scope events (jobs:read:progress) include progress data plus completion status:

{
  "jobId": "01JLQX...",
  "type": "progress",
  "data": { "percent": 65, "message": "Encoding video" },
  "timestamp": "2025-02-24T10:32:00.000Z"
}
TypeWhendata fieldjobs:read:progress
initImmediately on connect— (current status only)Delivered (includes progress if available)
progressWorker sent a progress update{ percent, message }Delivered
deliveredJob polled by a workerNot delivered
ackedWorker acknowledged; job is now runningNot delivered
checkpointWorker stored a checkpointWorker-defined payloadNot delivered
eventWorker stored a custom debug/admin eventWorker-defined payloadNot delivered
completedJob completed successfullyResult payload from workerDelivered (without data)
retriedJob failed, being re-queued{ error, attemptNumber }Not delivered
deadJob failed with no retries remaining{ error }Not delivered
killedJob force-stopped before completion{ reason }Not delivered
delivery_timeoutWorker polled but never acked in time; reverted to pendingNot delivered

With jobs:read:progress, only init, progress, and completed are delivered. completed omits data.

Message samples

Sent immediately on connect. With jobs:read, it includes current status; with jobs:read:progress, it includes the last known progress (if any).

{
  "jobId": "01JLQX...",
  "type": "init",
  "status": "running",
  "timestamp": "2025-02-24T10:31:05.000Z"
}

Sent each time the worker calls the progress endpoint. data always includes { percent, message }.

{
  "jobId": "01JLQX...",
  "jobName": "process-video",
  "type": "progress",
  "status": "running",
  "data": { "percent": 65, "message": "Encoding video" },
  "timestamp": "2025-02-24T10:32:00.000Z"
}

Sent when the worker marks the job complete. data contains the result payload with jobs:read; jobs:read:progress clients receive the same event without data.

{
  "jobId": "01JLQX...",
  "jobName": "process-video",
  "type": "completed",
  "status": "completed",
  "data": { "output_url": "s3://bucket/output.mp4", "duration_ms": 12340 },
  "timestamp": "2025-02-24T10:34:20.000Z"
}

Sent when the job fails with no retries remaining. data contains the final error. Delivered only with jobs:read.

{
  "jobId": "01JLQX...",
  "jobName": "process-video",
  "type": "dead",
  "status": "dead",
  "data": { "error": "FFmpeg exited with code 1: out of memory" },
  "timestamp": "2025-02-24T10:42:00.000Z"
}

Sent when a job is force-stopped before completion. Delivered only with jobs:read.

{
  "jobId": "01JLQX...",
  "jobName": "process-video",
  "type": "killed",
  "status": "killed",
  "data": { "reason": "manual stop" },
  "timestamp": "2025-02-24T10:36:00.000Z"
}

Sent when a job fails but still has retries remaining. data includes the error and attempt number. Delivered only with jobs:read.

{
  "jobId": "01JLQX...",
  "jobName": "process-video",
  "type": "retried",
  "status": "pending",
  "data": { "error": "Connection timeout", "attemptNumber": 1 },
  "timestamp": "2025-02-24T10:33:10.000Z"
}

Space Stream

GET /api/v1/realtime/spaces/:spaceId

Connect to a space-wide stream. You receive events for all jobs in the space: creation, status changes, progress, completions, failures, and kills.

The stream sends a connected event immediately on connect.

ParamRequiredDescription
tokenYesAPI key with jobs:read or higher. Must be scoped to this space if the key is space-scoped.
const ws = new WebSocket(
  `wss://emit.run/api/v1/realtime/spaces/${spaceId}?token=${apiKey}`
);

ws.onmessage = (e) => {
  const event = JSON.parse(e.data);
  // { jobId, jobName, type, status, data?, timestamp }
  console.log(`[${event.jobName}] ${event.type} → ${event.status}`);
};
async def watch_space(space_id: str, api_key: str):
    uri = f"wss://emit.run/api/v1/realtime/spaces/{space_id}?token={api_key}"

    async with websockets.connect(uri) as ws:
        async for message in ws:
            event = json.loads(message)
            print(f"[{event['jobName']}] {event['type']}{event['status']}")

asyncio.run(watch_space("01JLQX...", "emit_YOUR_KEY"))

The space stream uses the same event shape as the job stream, with the addition of jobName. Requires jobs:read or higher — so events always include full data.


Common Patterns

Submit-and-watch

Create a job on your backend and immediately stream progress back to the user. Use the space public token on the client side so no sensitive credentials are exposed:

// Backend: create job and return ID to client
app.post("/api/start-report", async (req, res) => {
  const job = await createJob({ name: "generate-report", payload: req.body });
  res.json({ jobId: job.id, token: process.env.SPACE_PUBLIC_TOKEN });
});

// Frontend: stream progress using public token
const { jobId, token } = await startReport(params);
const ws = new WebSocket(`wss://emit.run/api/v1/realtime/${jobId}?token=${token}`);

ws.onmessage = (e) => {
  const event = JSON.parse(e.data);
  if (event.type === "init" && event.progress) {
    updateProgressBar(event.progress.percent);
  }
  if (event.type === "progress") updateProgressBar(event.data.percent);
};

Dashboard live feed

Use the space stream with a jobs:read token to power a real-time operations dashboard:

const ws = new WebSocket(
  `wss://emit.run/api/v1/realtime/spaces/${spaceId}?token=${dashboardKey}`
);

ws.onmessage = (e) => {
  const event = JSON.parse(e.data);
  updateJobRow(event.jobId, { status: event.status, type: event.type });
};

On this page