emit.run

Quick Start

Get started with emit.run in minutes

Quick Start

This guide walks through the full flow: create a space, get an API key, submit a job, process it with a worker, and watch it complete.

1. Create a Space

In the dashboard, go to SpacesCreate Space. Give it a name like production or email-jobs.

2. Create API Tokens

Open your space and go to TokensCreate Token.

You'll typically want two tokens:

TokenScopesUsed by
my-producerjobs:createYour app that submits jobs
my-workerWorker preset (or jobs:worker)Your worker that processes jobs

Copy each key when it appears — it's shown only once.

3. Create a Job

Submit a job to the dispatch queue. The name is how your worker knows what to do; payload is the data it receives.

For bulk ingestion, you can also send an array of job objects to create multiple jobs in one request.

const res = await fetch(
  `https://emit.run/api/v1/spaces/${spaceId}/jobs`,
  {
    method: "POST",
    headers: {
      "x-api-key": process.env.EMIT_PRODUCER_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "send-email",
      payload: {
        to: "user@example.com",
        subject: "Welcome!",
        template: "onboarding",
      },
    }),
  }
);

const job = await res.json();
console.log("Created job:", job.id); // { id: "01JLQX...", status: "pending", ... }
curl -X POST https://emit.run/api/v1/spaces/YOUR_SPACE_ID/jobs \
  -H "x-api-key: emit_YOUR_PRODUCER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "send-email",
    "payload": {
      "to": "user@example.com",
      "subject": "Welcome!",
      "template": "onboarding"
    }
  }'
import requests

res = requests.post(
    f"https://emit.run/api/v1/spaces/{space_id}/jobs",
    headers={
        "x-api-key": EMIT_PRODUCER_KEY,
        "Content-Type": "application/json",
    },
    json={
        "name": "send-email",
        "payload": {
            "to": "user@example.com",
            "subject": "Welcome!",
            "template": "onboarding",
        },
    },
)

job = res.json()
print("Created job:", job["id"]) # { "id": "01JLQX...", "status": "pending", ... }

Optional: scheduled (delayed) jobs

Add scheduledFor to delay execution until a future time. The job stays in scheduled state until then:

{
  "name": "send-report",
  "payload": { "reportId": "abc123" },
  "scheduledFor": "2025-03-01T09:00:00.000Z"
}

See Scheduled jobs for details.

Optional: webhook callback

Add callbackUrl to get a POST notification when the job completes, goes dead, or is killed — no polling required:

{
  "name": "send-email",
  "payload": { "to": "user@example.com", "template": "onboarding" },
  "callbackUrl": "https://example.com/webhooks/jobs",
  "callbackHeaders": { "Authorization": "Bearer whsec_your_secret" }
}

See Callbacks for the full payload shape.

4. Build a Worker

A worker is a loop that polls for jobs, processes them, and reports back. Workers need no SDK — it's plain HTTP.

When reporting progress, the payload must be exactly { percent, message } where percent is 0..100 and message is user-facing.

const API = "https://emit.run/api/v1";
const KEY = process.env.EMIT_WORKER_KEY!;
const SPACE = process.env.EMIT_SPACE_ID!;
const headers = { "x-api-key": KEY, "Content-Type": "application/json" };

async function work() {
  while (true) {
    // 1. Poll for up to 5 jobs
    const pollRes = await fetch(`${API}/spaces/${SPACE}/jobs/poll`, {
      method: "POST",
      headers,
      body: JSON.stringify({ count: 5 }),
      // You can filter by one or more job types:
      // body: JSON.stringify({ count: 5, type: ["send-email", "send-sms"] }),
    });
    const { jobs } = await pollRes.json();

    if (jobs.length === 0) {
      await new Promise((r) => setTimeout(r, 2000)); // back off
      continue;
    }

    for (const job of jobs) {
      // 2. Ack — tells the system we're starting; starts the timeout timer
      await fetch(`${API}/jobs/${job.id}/ack`, { method: "POST", headers });

      try {
        // 3. Do the actual work
        const result = await processJob(job);

        // 4. Report success
        await fetch(`${API}/jobs/${job.id}/complete`, {
          method: "POST",
          headers,
          body: JSON.stringify({ result }),
        });
      } catch (err) {
        // 4. Report failure — retries automatically if attempts remain
        await fetch(`${API}/jobs/${job.id}/fail`, {
          method: "POST",
          headers,
          body: JSON.stringify({ error: String(err) }),
        });
      }
    }
  }
}

async function processJob(job: { name: string; payload: any }) {
  switch (job.name) {
    case "send-email":
      // send the email...
      return { sent: true };
    default:
      throw new Error(`Unknown job type: ${job.name}`);
  }
}

work();
import os
import time
import requests

API = "https://emit.run/api/v1"
KEY = os.environ["EMIT_WORKER_KEY"]
SPACE = os.environ["EMIT_SPACE_ID"]
HEADERS = {"x-api-key": KEY, "Content-Type": "application/json"}

def work():
    while True:
        # 1. Poll for up to 5 jobs
        res = requests.post(
            f"{API}/spaces/{SPACE}/jobs/poll",
            headers=HEADERS,
            json={"count": 5},
            # You can filter by one or more job types:
            # json={"count": 5, "type": ["send-email", "send-sms"]},
        )
        jobs = res.json()["jobs"]

        if not jobs:
            time.sleep(2)  # back off
            continue

        for job in jobs:
            # 2. Ack — tells the system we're starting; starts the timeout timer
            requests.post(f"{API}/jobs/{job['id']}/ack", headers=HEADERS)

            try:
                # 3. Do the actual work
                result = process_job(job)

                # 4. Report success
                requests.post(
                    f"{API}/jobs/{job['id']}/complete",
                    headers=HEADERS,
                    json={"result": result},
                )
            except Exception as e:
                # 4. Report failure — retries automatically if attempts remain
                requests.post(
                    f"{API}/jobs/{job['id']}/fail",
                    headers=HEADERS,
                    json={"error": str(e)},
                )

def process_job(job):
    if job["name"] == "send-email":
        # send the email...
        return {"sent": True}
    raise ValueError(f"Unknown job type: {job['name']}")

work()

If your control plane can stop jobs via POST /jobs/:id/kill, worker mutation calls like progress, checkpoint, event, and keepalive can return 409 with code: "JOB_KILLED". Treat that as a stop signal and exit work for that job immediately.

5. Watch Progress (Optional)

The recommended pattern for client-facing apps: your backend creates the job and returns the job ID, your frontend connects using the space's public token to stream live updates.

You can also fetch the latest progress from your backend using GET /api/v1/jobs/:jobId/progress with a jobs:read:progress token.

Every space comes with a pre-generated public token (jobs:read:progress scope) that's safe to embed in client-side code — it can only read progress updates on a single job (no job results, no status history, no space-wide feed).

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

// Backend: create the job, return its ID to the client
app.post("/api/send-email", async (req, res) => {
  const job = await fetch(`${API}/spaces/${SPACE}/jobs`, {
    method: "POST",
    headers: {
      "x-api-key": process.env.EMIT_PRODUCER_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ name: "send-email", payload: req.body }),
  }).then((r) => r.json());

  res.json({ jobId: job.id });
  // PUBLIC_TOKEN comes from your space dashboard — safe to expose to clients
});

// Frontend: watch the job using the public token
const { jobId } = await submitEmailRequest(formData);
const ws = new WebSocket(
  `wss://emit.run/api/v1/realtime/${jobId}?token=${SPACE_PUBLIC_TOKEN}`
);

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

  if (event.type === "init" && event.progress) {
    updateProgressBar(event.progress.percent);
    updateProgressLabel(event.progress.message);
  }

  if (event.type === "progress") {
    updateProgressBar(event.data.percent);
    updateProgressLabel(event.data.message);
  }
};

For completion, failure, or kill notifications, use webhooks or have your backend publish a final status to the client.

See WebSockets for the full event reference and space-level streams.

Next Steps

  • Authentication — Full scope reference and auth details
  • Jobs API — Complete endpoint reference with examples
  • Callbacks — Webhook notifications on job completion
  • WebSockets — Real-time streaming setup

On this page