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 Spaces → Create Space. Give it a name like production or email-jobs.
2. Create API Tokens
Open your space and go to Tokens → Create Token.
You'll typically want two tokens:
| Token | Scopes | Used by |
|---|---|---|
my-producer | jobs:create | Your app that submits jobs |
my-worker | Worker 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 Tokens → Public 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