Dispatch for Developers

Programmable agents.
REST API. SSE streaming.

Trigger workflows from your own systems. Fire webhooks, parse plans, stream live phase events, manage credentials. Every surface in the Dispatch UI is just a thin client over this API — and you can build on top of it.

Endpoints
29
Tool integrations
12
LLM providers
3 + BYO
Stream protocol
SSE
/ Authentication

Two ways to authenticate

Pick whichever matches the shape of your client. Public endpoints (parse, templates, triggers) skip auth entirely.

01
Available

Session cookie

Sign in via the UI or via /api/auth/login. The browser stores an httpOnly dispatch_session cookie (JWT, 30-day TTL). Send it on every request.

# Sign in once and persist the cookie:
curl -X POST https://dispatch.app/api/auth/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{"email":"you@example.com","password":"hunter2"}'

# Re-use it for every authenticated call:
curl https://dispatch.app/api/workflows \
  -b cookies.txt
02
Coming soon

Personal API token

Paste your token as Authorization: Bearer dsp_… on any request. Tokens carry the same scope as your session and can be revoked from /settings.

# Once API tokens ship:
curl https://dispatch.app/api/workflows \
  -H "Authorization: Bearer dsp_live_<dispatch_session>"
BYOK and zero token markup. Bring your own OpenAI / Anthropic / Google key in /api/credentials. We never see your prompts and never charge for tokens — that bill stays with your provider.
/ Errors

Predictable error envelope

Every failure returns JSON in the same shape, with an HTTP status that matches its category. Parse the body — never the status alone.

Error envelope
{
  "error":   string,           // human-readable summary
  "code":    string?,          // stable machine identifier (optional)
  "details": Record<string, unknown>?  // optional debugging payload
}
Status reference
  • 400
    Bad request
    Validation failed or required fields missing
  • 401
    Unauthorized
    Session cookie missing, expired, or invalid
  • 403
    Forbidden
    Workflow disabled or you don't own this resource
  • 404
    Not found
    Workflow / template / integration unknown
  • 409
    Conflict
    Email already registered, etc.
  • 429
    Rate limited
    Public parse cap exceeded — see Retry-After
  • 500
    Server error
    Unhandled — please retry or report
/ Rate limits

Limits during beta

Authenticated APIs are unmetered while we tune capacity. Public endpoints are throttled to keep abusers from torching your quota.

Public parse
3 / 5 min
per IP, anonymous

Returns 429 with a retryAfter (seconds) field once exceeded.

Authenticated APIs
No hard cap
current beta

We monitor outliers and will reach out before introducing per-account limits.

SSE streams
10 concurrent
per user

Open streams beyond this respond 429; close idle EventSources promptly.

/ Auth endpoints

Sessions

Create accounts, sign in, sign out, and resolve the current user from the cookie.

POST/api/auth/signup
Auth: none

Create a new account. Sets the dispatch_session cookie on success.

Request body
{ "email": string, "password": string, "name"?: string }
Response (200)
{ "user": { "id": string, "email": string, "name": string } }
Example
curl -X POST https://dispatch.app/api/auth/signup -c cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"hunter2","name":"You"}'
note409 if the email is already taken. Cookie is httpOnly, sameSite=lax, 30-day TTL.
POST/api/auth/login
Auth: none

Sign in with email + password. Sets dispatch_session cookie.

Request body
{ "email": string, "password": string }
Response (200)
{ "user": { "id": string, "email": string, "name": string } }
Example
curl -X POST https://dispatch.app/api/auth/login -c cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"hunter2"}'
note401 on bad credentials. Re-use the cookie for every authenticated call.
POST/api/auth/logout
Auth: none

Clear the session cookie. Always returns 200.

Response (200)
{ "ok": true }
Example
curl -X POST https://dispatch.app/api/auth/logout -b cookies.txt
GET/api/auth/me
Auth: none

Return the current session user, or { user: null } if not signed in.

Response (200)
{ "user": { "id": string, "email": string, "name": string } | null }
Example
curl https://dispatch.app/api/auth/me -b cookies.txt
noteAlways 200 — check user === null rather than the status code.
/ Workflows

CRUD over workflows

A workflow is a saved, named WorkflowPlan with a trigger config. These endpoints power the dashboard, history, and editor.

GET/api/workflows
Auth: session cookie

List all workflows belonging to the current user.

Response (200)
{ "workflows": SavedWorkflow[] }
Example
curl https://dispatch.app/api/workflows -b cookies.txt
POST/api/workflows
Auth: session cookie

Save a parsed plan as a workflow. Pass the plan returned from /api/parse.

Request body
{
  "plan":      WorkflowPlan,                          // required, must include id
  "schedule"?: "on_demand" | "scheduled" | "webhook"  // defaults on_demand
}
Response (200)
{ "workflow": SavedWorkflow }
Example
curl -X POST https://dispatch.app/api/workflows -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"plan":{"id":"...","name":"...","steps":[...]}}'
note400 if plan.id is missing. The persisted record adds runCount and lastRunAt.
GET/api/workflows/{id}
Auth: session cookie

Fetch a single workflow by id.

Response (200)
{ "workflow": SavedWorkflow }
Example
curl https://dispatch.app/api/workflows/wf_8a23 -b cookies.txt
note404 { error: "Not found" } if the workflow does not belong to you.
PATCH/api/workflows/{id}
Auth: session cookie

Partially update a workflow. Omitted fields are left untouched.

Request body
{
  "name"?:        string,
  "description"?: string,
  "enabled"?:     boolean,
  "cron"?:        string,                            // 5-field cron expression
  "trigger"?:     "manual" | "schedule" | "webhook",
  "steps"?:       WorkflowStep[]
}
Response (200)
{ "workflow": SavedWorkflow }
Example
curl -X PATCH https://dispatch.app/api/workflows/wf_8a23 -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"enabled":true,"cron":"0 9 * * 1-5","trigger":"schedule"}'
DELETE/api/workflows/{id}
Auth: session cookie

Permanently delete a workflow. Idempotent.

Response (200)
{ "ok": true }
Example
curl -X DELETE https://dispatch.app/api/workflows/wf_8a23 -b cookies.txt
POST/api/workflows/blank
Auth: session cookie

Create an empty Untitled Mission so the user can build it in the visual editor.

Response (200)
{ "workflow": SavedWorkflow }
Example
curl -X POST https://dispatch.app/api/workflows/blank -b cookies.txt
POST/api/workflows/{id}/run
Auth: session cookie

Synchronously execute a saved workflow. Returns the full run record.

Response (200)
{
  "run": {
    "id": string, "workflowId": string,
    "status": "ok" | "failed",
    "durationMs": number, "startedAt": number, "completedAt": number,
    "trigger": "manual", "steps": StepResult[]
  }
}
Example
curl -X POST https://dispatch.app/api/workflows/wf_8a23/run -b cookies.txt
notemaxDuration 120s. Long-running plans should use /api/execute (SSE) instead.
/ Compose & execute

From English to running plan

Parse natural language into a structured plan, then execute it synchronously or stream it phase by phase.

POST/api/parse
Auth: none

Translate natural language into a structured WorkflowPlan. Used by the composer.

Request body
{
  "input":         string,    // required, the brief in plain English
  "enabledTools"?: string[]   // restrict the planner to these tool ids
}
Response (200)
{ "plan": { "id": string, "createdAt": number, "name": string, "steps": WorkflowStep[], ... } }
Example
curl -X POST https://dispatch.app/api/parse \
  -H "Content-Type: application/json" \
  -d '{"input":"Summarize my Gmail every morning into a Notion page."}'
noteCalls a Qwen-backed planner. maxDuration 60s. 500 with { error, raw } if the LLM output cannot be JSON-parsed.
POST/api/execute
Auth: session cookie

Execute a plan and stream phase events as SSE.

Request body
{ "plan": WorkflowPlan, "workflowId"?: string }
Response (200)
text/event-stream
---
event: step_start    | data: { "stepId": "...", "tool": "gmail", "op": "search" }
event: step_log      | data: { "stepId": "...", "line": "..." }
event: step_done     | data: { "stepId": "...", "status": "ok", "durationMs": 412 }
event: run_complete  | data: { "status": "ok" | "failed", "runId": "...", "error"?: string }
Example
curl -N -X POST https://dispatch.app/api/execute -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"plan":{...}}'
noteUse EventSource or fetch + getReader. Cache-Control: no-cache. maxDuration 120s. workflowId bumps the saved run count.
POST/api/public/parse
Auth: none

Public, IP-rate-limited parser used by anonymous demos. No session required.

Request body
{ "input": string }
Response (200)
{ "plan": WorkflowPlan } | { "error": "rate_limited", "retryAfter": number }
Example
curl -X POST https://dispatch.app/api/public/parse \
  -H "Content-Type: application/json" \
  -d '{"input":"Watch a GitHub repo and ping me on stale PRs."}'
noteLimit: 3 requests / 5 min / IP. Identical response shape to /api/parse on success.
/ Templates

Recipes you can fork

Read-only catalogue plus a clone endpoint that drops a fresh, disabled copy into the user's workspace.

GET/api/templates
Auth: none

List built-in templates. Optional ?category= filter.

Response (200)
{
  "templates": [{ "slug": string, "name": string, "tagline": string, "category": string,
                 "icon": string, "highlightTools": ToolName[], "estimatedRuntime": number }]
}
Example
curl "https://dispatch.app/api/templates?category=Productivity"
GET/api/templates/{slug}
Auth: none

Fetch a single template, including its full step graph.

Response (200)
{ "template": Template }
Example
curl https://dispatch.app/api/templates/morning-inbox-digest
note404 if the slug is unknown.
POST/api/templates/{slug}/clone
Auth: session cookie

Clone a template into the user’s workspace as a fresh, disabled workflow.

Response (200)
{ "workflow": SavedWorkflow }
Example
curl -X POST https://dispatch.app/api/templates/morning-inbox-digest/clone -b cookies.txt
noteCloned workflow lands with enabled:false so the user can review before turning it on.
/ Credentials

LLM provider keys (BYOK)

Save, test, and switch between OpenAI, Anthropic, and Google keys. Keys are encrypted at rest and never returned in plaintext.

GET/api/credentials
Auth: session cookie

List saved LLM provider credentials. Keys are returned masked.

Response (200)
{
  "default":   "openai" | "anthropic" | "google" | null,
  "providers": [{ "provider": Provider, "maskedKey": string, "model"?: string,
                 "createdAt": number, "updatedAt": number }]
}
Example
curl https://dispatch.app/api/credentials -b cookies.txt
PUT/api/credentials/{provider}
Auth: session cookie

Save or replace the API key for a provider.

Request body
{ "apiKey": string, "model"?: string }
Response (200)
{ "provider": string, "maskedKey": string, "model"?: string, "createdAt": number, "updatedAt": number }
Example
curl -X PUT https://dispatch.app/api/credentials/openai -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"apiKey":"sk-...","model":"gpt-4o"}'
noteprovider must be openai, anthropic, or google. Keys are encrypted at rest.
DELETE/api/credentials/{provider}
Auth: session cookie

Remove a stored provider key.

Response (200)
{ "default": ... , "providers": [...] }  // sanitized credentials
Example
curl -X DELETE https://dispatch.app/api/credentials/openai -b cookies.txt
POST/api/credentials/{provider}
Auth: session cookie

Test a provider key. Pass apiKey to test a candidate, or send empty body to test the saved key.

Request body
{ "apiKey"?: string }
Response (200)
{ "ok": boolean, "info"?: string, "error"?: string }
Example
curl -X POST https://dispatch.app/api/credentials/openai -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"apiKey":"sk-..."}'
noteAlways 200 unless the request itself is malformed. Inspect the ok flag.
PUT/api/credentials/default
Auth: session cookie

Set the default LLM provider used when a workflow does not specify one.

Request body
{ "provider": "openai" | "anthropic" | "google" }
Response (200)
{ "default": string, "providers": [...] }
Example
curl -X PUT https://dispatch.app/api/credentials/default -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"provider":"anthropic"}'
/ Integrations

12 first-party tools

Each tool has its own field shape (declared by the integration registry). PUT to save, POST to test, DELETE to remove.

GET/api/integrations
Auth: session cookie

List saved integrations plus the catalogue of available tools.

Response (200)
{ "integrations": Integration[],   // sanitized — secrets redacted
  "available":    IntegrationDef[] }  // catalog: tool id, label, fields
Example
curl https://dispatch.app/api/integrations -b cookies.txt
PUT/api/integrations/{tool}
Auth: session cookie

Save or replace integration fields. Optionally test before saving.

Request body
{
  "fields": { [key: string]: string },  // all required fields must be present
  "test"?:  boolean                      // if true, tested before saving
}
Response (200)
{ "integration": SanitizedIntegration, "test"?: { "ok": boolean, "info"?: string } }
Example
curl -X PUT https://dispatch.app/api/integrations/slack -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"fields":{"botToken":"xoxb-..."},"test":true}'
notetool ids: gmail, google_calendar, google_drive, notion, slack, discord, github, linear, stripe, x, http, openai. 400 if a required field is blank or if test:true and the test fails.
DELETE/api/integrations/{tool}
Auth: session cookie

Remove a saved integration.

Response (200)
{ "integrations": Integration[], "available": IntegrationDef[] }
Example
curl -X DELETE https://dispatch.app/api/integrations/slack -b cookies.txt
POST/api/integrations/{tool}
Auth: session cookie

Test an integration. Pass fields to test new credentials, or send empty body to test the saved record.

Request body
{ "fields"?: { [key: string]: string } }
Response (200)
{ "ok": boolean, "info"?: string, "error"?: string }
Example
curl -X POST https://dispatch.app/api/integrations/slack -b cookies.txt \
  -H "Content-Type: application/json" \
  -d '{"fields":{"botToken":"xoxb-..."}}'
/ Triggers

Webhooks & cron

A workflow fires when its webhook URL is hit, when its cron expression matches, or when a user clicks Run.

POST/api/triggers/{secret}
Auth: none

Public webhook entry. Fires the workflow associated with the opaque secret.

Response (200)
{ "accepted": true, "runId": string, "status": "ok" | "failed", "durationMs": number, "workflowId": string }
Example
curl -X POST https://dispatch.app/api/triggers/8a23-... \
  -H "Content-Type: application/json" \
  -d '{"orderId":"ord_42"}'
noteTreat the secret as opaque — do not log it. 404 if the secret is unknown, 403 if the workflow is disabled. The request body is available throughout the run as {{trigger.*}}.
GET/api/triggers/{secret}
Auth: none

Health-check / browser-friendly variant. Fires the same run.

Response (200)
{ "accepted": true, "runId": ..., "status": ..., "durationMs": ..., "workflowId": ... }
Example
curl https://dispatch.app/api/triggers/8a23-...
noteUseful for verifying a webhook URL from a browser. In production, prefer POST.
GET/api/cron
Auth: CRON_SECRET

Internal Vercel Cron entry. Fires every workflow whose cron matches the current minute.

Response (200)
{
  "timestamp": string, "scheduled": number, "fired": number,
  "runs": [{ "workflowId": string, "runId"?: string, "status": "ok" | "failed" | "error" }]
}
Example
curl https://dispatch.app/api/cron -H "Authorization: Bearer $CRON_SECRET"
noteRequires Authorization: Bearer ${CRON_SECRET}. Vercel Cron sends this automatically. Configure in vercel.json.
/ History

Audit + replay

Every run is recorded with input, output, latency, and outcome. Replay or retry from the dashboard or via API.

GET/api/history
Auth: session cookie

Most recent 50 run records for the current user. Optional ?workflowId= filter.

Response (200)
{
  "history": [{ "id": string, "workflowId": string,
                "status": "ok" | "failed",
                "trigger": "manual" | "scheduled" | "webhook",
                "durationMs": number, "startedAt": number, "completedAt": number,
                "steps": StepResult[] }]
}
Example
curl "https://dispatch.app/api/history?workflowId=wf_8a23" -b cookies.txt
POST/api/runs/{id}/replay
Auth: session cookie

Re-run an exact past execution with the same inputs. Useful for debugging idempotent workflows.

Response (200)
{ "run": RunRecord }
Example
curl -X POST https://dispatch.app/api/runs/run_8a23/replay -b cookies.txt
noteReplay reuses the historical trigger payload; side effects (Slack messages, Stripe writes) WILL fire again.
POST/api/runs/{id}/retry
Auth: session cookie

Retry a failed run from the first failing step. Earlier successful steps are skipped.

Response (200)
{ "run": RunRecord }
Example
curl -X POST https://dispatch.app/api/runs/run_8a23/retry -b cookies.txt
noteOnly valid for runs with status "failed". 400 if the original run succeeded.
/ Streaming

Consuming /api/execute

/api/execute is a Server-Sent Events stream. Each phase emits structured events; consume them with EventSource or fetch + a reader.

Browser — fetch + ReadableStream reader
async function runPlan(plan) {
  const res = await fetch('/api/execute', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ plan }),
  })
  if (!res.body) throw new Error('No stream')

  const reader = res.body
    .pipeThrough(new TextDecoderStream())
    .getReader()

  let buffer = ''
  while (true) {
    const { value, done } = await reader.read()
    if (done) break
    buffer += value

    const parts = buffer.split('\n\n')
    buffer = parts.pop() ?? ''

    for (const chunk of parts) {
      const lines = chunk.split('\n')
      const event = lines.find(l => l.startsWith('event: '))?.slice(7)
      const data  = lines.find(l => l.startsWith('data: '))?.slice(6)
      if (!event || !data) continue
      handleEvent(event, JSON.parse(data))
    }
  }
}
Event types
  • step_start
    Step is about to execute. Payload: stepId, tool, op.
  • step_log
    Per-line log output from the step. Payload: stepId, line.
  • step_done
    Step finished. Payload: stepId, status, durationMs, result?, error?.
  • step_mode
    Step switched mode (e.g. dry-run → live). Payload: stepId, mode.
  • phase_complete
    A whole phase (parallel group) finished. Payload: phaseIndex.
  • run_complete
    Terminal event. Payload: status, runId, error?. Always close after this.
  • error
    Out-of-band failure during streaming. Payload: error.
Node — EventSource via the eventsource package
import EventSource from 'eventsource'

const es = new EventSource('https://dispatch.app/api/execute', {
  method: 'POST',
  headers: { Cookie: 'dispatch_session=' + token },
  payload: JSON.stringify({ plan }),
})

es.addEventListener('step_done',     (e) => console.log('done', JSON.parse(e.data)))
es.addEventListener('run_complete', (e) => { es.close() })
es.addEventListener('error',         (e) => { es.close() })
/ Webhook security

Verify what you receive

When you create a webhook trigger, the URL ends with …/api/triggers/{secret}. Treat the secret as opaque — do not log it, do not put it in error reports, do not commit it to source.

Signed deliveries

Every outbound delivery from Dispatch carries a signature header. Verify it on your end before trusting the body.

  • X-Dispatch-SignatureHMAC-SHA256 of the raw body, hex-encoded.
  • X-Dispatch-TimestampUnix ms. Reject anything more than 5 minutes off.
  • X-Dispatch-Eventevent name, e.g. run.completed.
Verify a webhook (Node)
import crypto from 'node:crypto'

export function verify(req, secret) {
  const sig = req.headers['x-dispatch-signature']
  const ts  = req.headers['x-dispatch-timestamp']
  if (!sig || !ts) return false

  // Reject replays older than 5 minutes.
  if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) {
    return false
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(req.rawBody)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(sig,      'hex'),
  )
}
Heads up. Webhook signing is rolling out alongside an upcoming hardening pass. Until then, treat the URL secret as your bearer token: rotate it from the workflow settings if it ever appears in a log.
/ SDKs & clients

Use any HTTP client

Until first-party SDKs ship, native fetch and requests work great. The surface area is small — five-ish endpoints will cover most use cases.

JavaScript / TypeScript
// npm: native fetch — no dependencies
const base = 'https://dispatch.app'

async function login(email: string, password: string) {
  const r = await fetch(`${base}/api/auth/login`, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify({ email, password }),
    credentials: 'include',
  })
  if (!r.ok) throw new Error((await r.json()).error)
  return r.json()
}

async function listWorkflows() {
  const r = await fetch(`${base}/api/workflows`, {
    credentials: 'include',
  })
  return r.json()
}

async function plan(input: string) {
  const r = await fetch(`${base}/api/parse`, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify({ input }),
  })
  return r.json()
}
Python — requests
# pip install requests
import requests

base = 'https://dispatch.app'
s    = requests.Session()

def login(email, password):
    r = s.post(f'{base}/api/auth/login',
               json={'email': email, 'password': password})
    r.raise_for_status()
    return r.json()

def list_workflows():
    return s.get(f'{base}/api/workflows').json()

def plan(prompt):
    r = s.post(f'{base}/api/parse', json={'input': prompt})
    return r.json()

def run(workflow_id):
    return s.post(f'{base}/api/workflows/{workflow_id}/run').json()
Official SDKs coming soon

TypeScript and Python first. Want one in your language? Open an issue with your stack and use case.

Open an issue
Read the spec

Build on
Dispatch.

Free forever. BYOK. Five minutes from sign-up to a curl that actually does something.