Relay's JSON API - the same endpoints the app's own dashboard and canvas view call. All request/response bodies are JSON unless noted.
Every /api/* endpoint below requires a valid session cookie, set by logging in through the web app (POST /login) or completing signup (POST /signup) / install (POST /install). There is no separate API token today - the JSON API is meant for this app's own frontend, not third-party integration, though every endpoint here is plain JSON over HTTP and easy to script against once you hold a session cookie.
/api/workflows/:id/runRuns every agent step in a workflow in order, synchronously, and returns the full result once every step is done (or one fails).
{ "input": "the task to run" }{ "ok": true, "runId": "...", "status": "completed" | "failed",
"steps": [ { "agentName": "...", "outputLabel": "...", "output": "...", "engine": "mock" | "gemini" | "claude" } ] }400 if input is missing or the workflow has no steps. 402 (`{ ok:false, upgradeRequired:true }`) if the workspace is at its plan's monthly run limit. 404 if the workflow doesn't exist in your workspace.
/api/workflows/:id/run/stream?input=...The same pipeline, streamed live over Server-Sent Events as each agent step actually finishes - this is what the canvas view's "Run & watch" panel uses. Input is a query parameter (not a body) because EventSource only issues GET requests.
(none - GET request; the task text is the `input` query parameter)
A text/event-stream. Events, in order:
run_start { runId, totalSteps }
step_start { stepId, index, agentName }
step_done { stepId, index, agentName, outputLabel, output, engine }
step_error { stepId, index, agentName, error } (run stops here if it fires)
run_error { error, upgradeRequired? } (no steps ran at all)
run_done { runId, status: "completed" | "failed" }A run_error event (not an HTTP error code, since the response is already an open stream) covers a missing input, no steps configured, or the usage limit being reached.
/api/workflows/:id/stepsAppends a new agent-type step to the end of a workflow, with sensible defaults. Switch it to an integration step afterward via the update endpoint below.
(none)
{ "ok": true, "step": { ...the new step row... } }/api/workflow-steps/:idUpdates one step's editable fields - an AI-agent step (agent_name/role_prompt) or an integration step (step_type:'webhook' + webhook_config, a JSON string of {connectionId, path, body} - see Integrations below for what body can reference).
{ "agent_name"?: "...", "role_prompt"?: "...", "output_label"?: "...", "step_type"?: "agent" | "webhook", "webhook_config"?: "{...JSON string...}" }{ "ok": true, "step": { ... } }/api/workflow-steps/:id/positionUpdates a step's canvas (x, y) position. Purely visual - never changes run order.
{ "x": 300, "y": 140 }{ "ok": true, "step": { ... } }/api/workflow-steps/:id/moveSwaps a step's execution order with its immediate neighbor. Never changes canvas position.
{ "direction": "up" | "down" }{ "ok": true, "steps": [ ...the workflow's full step list, reordered... ] }/api/workflow-steps/:id/deleteRemoves one step from its workflow.
(none)
{ "ok": true }Relay is the orchestrator in this suite: a workflow can mix ordinary AI-agent steps with "integration" steps that call another product's /integrations/v1/* endpoint (see Orbit's, Rivo's, Bloom's or Nudge's own API docs for what each one accepts), and a workflow can be started either by a person clicking Run or by another product's event arriving here. There is only one HTTP endpoint on this side - the inbound event receiver below - since outbound integration calls happen from inside a workflow run, not through a separate API. Connections and Triggers are managed from Settings, not the JSON API.
/integrations/v1/events/:triggerIdAnother product's webhook subscription delivers an event here. Verifies the X-Signal-Signature header (Stripe-style: t=
{ "event_type": "...", "payload": { ...whatever the sending product published... }, "sent_at": "..." } (raw JSON, signed - not session/API-key authenticated){ "ok": true, "runId": "...", "status": "completed" | "failed", "steps": [...] } on a successful run (matches POST /api/workflows/:id/run's shape).401 if the signature is missing, malformed, or does not match the trigger's configured secret (including when no secret has been pasted in yet). 404 for an unknown trigger id. 400 for a non-JSON body. Also returns whatever runWorkflowCore itself would return on failure (400/402) - e.g. the workflow has no steps, or the workspace is at its plan's run limit.
/billing/checkoutStarts a subscription upgrade/downgrade. Redirects (302) to a real Stripe Checkout page when Stripe is configured (STRIPE_SECRET_KEY + a price ID for the plan); otherwise falls back to updating the stored plan directly ("demo mode") and redirects to Settings. Session-authenticated, form-encoded (not JSON) since it's posted by a real HTML form.
plan=starter | growth | scale (form-encoded)
302 redirect (to Stripe Checkout, or back to Settings in demo mode).
/billing/portalRedirects to a real Stripe Billing Portal session for the current workspace (requires Stripe to be configured and the workspace to already have a Stripe customer).
(none)
302 redirect to Stripe, or back to Settings with an error.
/webhooks/stripeStripe calls this directly - not session-authenticated, verified instead via the Stripe-Signature header (HMAC-SHA256, requires STRIPE_WEBHOOK_SECRET). Handles checkout.session.completed, customer.subscription.updated/created, and customer.subscription.deleted to keep each workspace's plan/subscription_status/current_period_end in sync.
Raw Stripe event JSON (sent by Stripe, not by you).
{ "received": true } on success, or { "error": "..." } with 400 on a bad/missing signature.