API reference
Every HTTP endpoint Setu exposes — the browser-facing REST API under /api/v1, the outbound edge-agent protocol under /agent/v1, and the machine-to-machine operational routes. Method, path, auth, request/response shape, and the status codes each handler actually returns.
Setu is a Go control plane (a single chi router) split into three trust boundaries. The console REST API under /api/v1 is what the Next.js console calls; it is authenticated by an httpOnly setu_session cookie. The agent protocol under /agent/v1 is a separate boundary for edge runtimes, authenticated by an agent key, not a session. A handful of unauthenticated operational routes (plus one token-guarded /internal route) sit at the root.
Conventions
| Base paths | /api/v1 (console), /agent/v1 (agents), root (/healthz, /readyz, /metrics, /internal/*) |
|---|---|
| Content type | application/json in and out (SSE endpoints emit text/event-stream; artifact downloads emit application/x-ndjson) |
| Session auth | httpOnly setu_session cookie, minted by signup/login and cleared by logout |
| Agent auth | Authorization: Bearer <agent-key> |
| Roles | owner > admin > editor > viewer. Reads: any member. Content mutations: editor+. Workspace/member/invite admin: admin+. Delete workspace: owner. |
| Error envelope | JSON { "error": "…" } with the matching HTTP status |
| Success bodies | a DTO or list; deletes and side-effect writes usually return 204 |
| Paging | lists are either a bare array, an offset pager { items, total } (?page=N), or a keyset pager { items, nextCursor } (runs) |
401 {"error":"unauthenticated"}. Resources outside your active workspace — and agent jobs not owned by the calling agent — return 404 rather than 403, so the API never leaks the existence of things you can’t see. Mutations you lack the role for return 403; conflicting state (last owner, non-empty workspace, referenced connection, active run) returns 409. JSON bodies are decoded with DisallowUnknownFields, so an unexpected key is a 400.config is stored verbatim: secret references like ${secret.NAME} are kept as-is and resolved late — in the cloud vault, or on the edge for agent-placed connections. The named secret store is gated by SETU_SECRET_KEY; without it, writing a secret returns 412. CORS is an allowlist (SETU_CORS_ORIGINS); only listed origins get credentialed responses.Auth & session
signup, login, and logout are the only /api/v1 routes registered outside the session middleware — they mint or clear the setu_session cookie (HttpOnly, Path=/; SameSite=Lax by default, or None; Secure when SETU_COOKIE_CROSS_SITE=true). Everything below requires a live session.
/api/v1/auth/signupnone{ "email": "you@acme.com", "password": "correct horse", "name": "Ada" }{
"user": { "id": "…", "email": "you@acme.com", "name": "Ada" },
"workspace": { "id": "…", "name": "Ada's workspace", "role": "owner" }
}Password must be ≥ 8 chars. The very first account ever becomes owner of the Default workspace; everyone else gets their own “<name>’s workspace”. A duplicate email is 409; a malformed one is 400.
/api/v1/auth/loginnone{ "email": "you@acme.com", "password": "correct horse" }{
"user": { "id": "…", "email": "you@acme.com", "name": "Ada" },
"workspace": { "id": "…", "name": "Default", "role": "owner" }
}Unknown email and wrong password both return the same 401 invalid email or password (no account enumeration). A user with no membership gets 403 account has no workspace. The first membership becomes the active workspace.
/api/v1/auth/logoutcookie sessionDeletes the server-side session and clears the cookie. Idempotent.
{ "ok": true }/api/v1/auth/mecookie session{
"user": { "id": "…", "email": "you@acme.com", "name": "Ada", "avatarUrl": "…" },
"workspace": { "id": "…", "name": "Default", "role": "owner", "avatarUrl": "…" },
"memberships": [ { "id": "…", "name": "Default", "role": "owner" } ],
"preferences": {}
}/api/v1/auth/switch-workspacecookie session{ "workspaceId": "…" }{ "id": "…", "name": "Acme", "role": "editor" }Not a member of the target → 403 you are not a member of that workspace.
/api/v1/auth/profilecookie session{ "name": "Ada Lovelace" }{ "id": "…", "email": "you@acme.com", "name": "Ada Lovelace", "avatarUrl": "…" }/api/v1/auth/passwordcookie session{ "currentPassword": "…", "newPassword": "…" }New password must be ≥ 8 chars and differ from the current one; a wrong currentPassword is a 400. Succeeds by keeping the current session and revoking every other session for the user.
/api/v1/auth/preferencescookie session{ "theme": "dark", "sidebar": "expanded" }Stored verbatim. A non-object body, or one over 64 KB, is a 400.
/api/v1/auth/avatarcookie session{ "avatar": "data:image/png;base64,…" }Accepts an https:// URL or a PNG/JPEG/WebP data URL, up to ~1 MB.
/api/v1/auth/avatarcookie sessionWorkspaces & members
Workspace and member administration is gated by role, not by the generic editor gate: renames, avatars, and member changes require admin+; deleting a workspace requires owner. Under-privileged calls are 403; non-members get 404 workspace not found.
/api/v1/workspacescookie session{ "name": "Acme" }{ "id": "…", "name": "Acme", "slug": "acme", "role": "owner" }/api/v1/workspaces/{id}cookie session · admin+{ "name": "Acme Inc" }{ "id": "…", "name": "Acme Inc", "slug": "acme-inc" }/api/v1/workspaces/{id}cookie session · ownerRefuses with 409 while the workspace still holds connections, pipelines, or datasets — empty it first. On success, 204.
/api/v1/workspaces/{id}/avatarcookie session · admin+{ "avatar": "https://…/logo.png" }/api/v1/workspaces/{id}/avatarcookie session · admin+/api/v1/workspaces/{id}/leavecookie session · memberAny member may leave; the sole remaining owner is blocked with 409. Returns 204.
/api/v1/workspaces/{id}/memberscookie session · member[ { "userId": "…", "name": "Ada", "email": "…", "role": "owner", "joinedAt": "…", "avatarUrl": "…" } ]/api/v1/workspaces/{id}/members/{userId}cookie session · admin+{ "role": "editor" }{ "userId": "…", "role": "editor" }You can’t set a role at or above your own, or act on a member who outranks you (403); demoting the last owner is 409. Fires a member.role_changed notification.
/api/v1/workspaces/{id}/members/{userId}cookie session · admin+Blocked (403) unless you outrank the target (or it’s yourself); removing the last owner is 409. Returns 204 and notifies the removed user.
Invite links & direct invitations
Two mechanisms: reusable invite links (a hashed token, optional expiry and use-cap) and per-user invitations to an existing Setu account. Both minting paths require admin+ and forbid inviting at or above your own role.
/api/v1/invites/{token}nonePublic peek so the accept page can render before login.
{ "valid": true, "workspaceName": "Acme", "role": "editor" }/api/v1/invites/{token}/acceptcookie sessionJoins the workspace and switches your active session to it. Unknown token → 404; expired, revoked, or exhausted → 410 Gone. Re-accepting as an existing member is idempotent and consumes no use.
{ "workspaceId": "…", "workspaceName": "Acme", "role": "editor" }/api/v1/workspaces/{id}/invitescookie session · admin+[ { "id": "…", "role": "editor", "expiresAt": "…", "createdAt": "…", "uses": 0, "maxUses": 5 } ]/api/v1/workspaces/{id}/invitescookie session · admin+{ "role": "editor", "expiresInDays": 7, "maxUses": 5 }{
"id": "…", "role": "editor", "expiresAt": "…", "createdAt": "…",
"uses": 0, "maxUses": 5,
"token": "…", "url": "https://app…/invite/…"
}/api/v1/workspaces/{id}/invites/{inviteId}cookie session · admin+/api/v1/workspaces/{id}/invitationscookie session · admin+{ "email": "bob@acme.com", "role": "viewer" }{ "id": "…", "role": "viewer", "status": "pending", "inviteeEmail": "bob@acme.com" }No account for that email, already a member, or already invited → 409. The invitee gets an invite.received notification (transactional).
/api/v1/invitations/{id}/acceptcookie session · invitee{ "workspaceId": "…", "workspaceName": "Acme", "role": "viewer" }Only the invitee may accept (else 404); a non-pending invitation is 409.
/api/v1/invitations/{id}/declinecookie session · inviteeReturns 204 and notifies the inviter. Same ownership/state guards as accept.
Connectors & connections
Connectors are the declarative catalog; the console renders connection forms straight from their specs. Connections are saved instances with a placement of cloud or agent. Agent-placed connections push reachability checks (test, discover, preview) out to the edge runtime and return a job id to poll.
/api/v1/connectorscookie sessionEvery registered connector spec — fields, and source/sink support flags.
/api/v1/connectors/{kind}/testcookie sessionTests a not-yet-saved config. Secret refs are resolved first; nothing is persisted. role defaults to source (sink for write-only connectors).
{ "config": { "host": "…", "password": "${secret.PG_PW}" }, "role": "source" }{ "ok": true }Unknown connector → 400; a failed live probe → 502.
/api/v1/connectionscookie sessionBare array by default; ?archived=true for the archive; ?page=N (+ ?q, ?limit) → { items, total }.
[
{
"id": "…", "name": "Prod Postgres", "provider": "postgres",
"config": { "host": "…" }, "placement": "cloud",
"agentId": null, "createdAt": "2026-01-02T…Z"
}
]/api/v1/connectionscookie session · editor+{
"name": "Prod Postgres",
"provider": "postgres",
"config": { "host": "db", "password": "${secret.PG_PW}" },
"placement": "cloud"
}{ "id": "…", "name": "Prod Postgres", "provider": "postgres", "config": { … }, "placement": "cloud", "createdAt": "…" }name and provider are required; an agent placement whose agentId isn’t an agent in this workspace is a 400.
/api/v1/connections/{id}cookie session · editor+Name, config, and placement are editable; the provider (connector kind) is immutable. Unknown id → 404.
/api/v1/connections/{id}cookie session · editor+Archives by default (soft delete, curation preserved); ?purge=true hard-deletes and cascades its datasets. A connection still referenced by live pipelines (archive) or any pipelines (purge) → 409. Success 204.
/api/v1/connections/{id}/restorecookie session · editor+Brings an archived connection and its datasets back. 204.
/api/v1/connections/{id}/testcookie sessionProbes the stored config without moving data. Agent-placed → runs on the runtime synchronously.
{ "ok": true }/api/v1/connections/{id}/discovercookie session · editor+Lists the source’s streams and reconciles them into the dataset catalog atomically.
{ "summary": { "added": 3, "updated": 1, "removed": 0 }, "datasets": [ … ] }{ "jobId": "…", "async": true }/api/v1/connections/{id}/previewcookie sessionReads a bounded sample from one source stream. limit defaults to 50, capped at 200. Empty stream → first discovered stream.
{ "stream": "public.orders", "limit": 50 }{
"stream": "public.orders",
"fields": [ { "name": "id", "type": "int64", "nullable": false } ],
"rows": [ [ 1, "…" ] ],
"truncated": false
}Datasets, folders & mapping
/api/v1/datasetscookie sessionFilters: connectionId, folderId, tag, q. Offset paging via limit (default 100, max 500) + offset.
{
"items": [
{
"id": "…", "connectionId": "…", "provider": "postgres",
"physicalName": "public.orders", "displayName": "Orders",
"type": "table", "schema": { … }, "rowEstimate": 12000,
"status": "active", "folderId": null, "tags": [ "sales" ], "defined": false
}
],
"total": 42
}/api/v1/datasets/tagscookie session{ "tags": [ "sales", "pii" ] }/api/v1/datasets/{id}cookie sessionFull datasetDTO. Unknown id → 404 dataset not found.
/api/v1/datasets/{id}/schemacookie sessionRaw stored schema JSON (or {}), written unwrapped.
/api/v1/datasets/{id}/usagecookie session{ "pipelines": [ { "id": "…", "name": "Orders → Warehouse" } ] }/api/v1/datasets/{id}/previewcookie sessionBounded row sample for the dataset’s underlying stream (previewDTO shape). Agent-placed connection → runtime passthrough.
{ "limit": 50 }/api/v1/datasets/{id}/profilecookie sessionSamples up to 500 rows and computes per-column stats.
{
"sampledRows": 500, "truncated": true,
"columns": [
{ "name": "amount", "type": "float64", "nullable": false,
"nulls": 0, "nullPct": 0, "distinct": 480, "min": "1.5", "max": "990" }
]
}/api/v1/datasets/{id}cookie session · editor+Any subset. folderId is tri-state: omit = keep, null/"" = move to root, uuid = move into folder.
{ "displayName": "Orders", "folderId": null, "tags": [ "sales" ] }/api/v1/datasets/{id}cookie session · editor+/api/v1/connections/{id}/datasets/describecookie sessionPreviews a defined dataset (e.g. a SQL query) against a source connection without saving it. A connector that doesn’t support defined datasets → 422.
{ "definition": { "query": "select * from orders where region = 'EU'" } }{ "stream": "preview", "fields": [ … ], "rows": [ … ], "truncated": true }/api/v1/connections/{id}/datasetscookie session · editor+Persists a defined dataset. name required; a duplicate name → 409; unsupported connector → 422.
{ "name": "EU Orders", "definition": { "query": "select …" } }{ "id": "…", "physicalName": "EU Orders", "defined": true, "definition": { … }, … }/api/v1/folderscookie session[ { "id": "…", "name": "Sales", "parentId": null } ]/api/v1/folderscookie session · editor+{ "name": "Sales", "parentId": null }/api/v1/folders/{id}cookie session · editor+{ "parentId": "…" }/api/v1/folders/{id}cookie session · editor+/api/v1/mappings/suggestcookie sessionStateless, deterministic source→target column alignment with confidence + reason. No data is read or written.
{
"source": [ { "name": "cust_id", "type": "int64" } ],
"target": [ { "name": "customer_id", "type": "int64" } ]
}{ "suggestions": [ { "source": "cust_id", "target": "customer_id", "confidence": 0.92, "reason": "…" } ] }Operators
/api/v1/operatorscookie sessionEvery operator spec with its resolved JSON Schema — the builder palette is data-driven from this.
/api/v1/operators/{type}/schemacookie sessionThe JSON Schema for one operator’s config, written unwrapped. Unknown type → 404.
/api/v1/operators/expression/validatecookie session{ "expr": "amount * 1.1", "columns": [ { "name": "amount", "type": "float64" } ] }{ "ok": true }Pipelines & schedules
A pipeline’s canonical form is a linear spec Document (source → operators → destination). Create/update validate the Document through a shared gate (Compile + incremental-sync rules); a diagnostic surfaces as a 400 shaped "NodeID: message".
/api/v1/pipelinescookie session?archived=true for the archive; ?page=N (+ ?q) for numbered paging; otherwise a bare array.
/api/v1/pipelinescookie session · editor+{
"name": "Orders → Warehouse",
"sourceConnectionId": "…",
"targetConnectionId": "…",
"mode": "append"
}{
"id": "…", "name": "Orders → Warehouse",
"sourceConnectionId": "…", "targetConnectionId": "…",
"spec": { … }, "createdAt": "…"
}/api/v1/pipelines/{id}cookie session/api/v1/pipelines/{id}cookie session · editor+spec (the full Document) is required; name is optional (blank keeps the current one). Connections are immutable. Validation failure → 400.
{ "name": "Orders → Warehouse", "spec": { … } }/api/v1/pipelines/{id}cookie session · editor+Archives; ?purge=true hard-deletes and cascades run history (events, quarantine, state). 204.
/api/v1/pipelines/{id}/restorecookie session · editor+/api/v1/pipelines/{id}/runscookie sessionAll runs for the pipeline (array of runDTO).
/api/v1/pipelines/{id}/runcookie session · editor+?dryRun=true validates and reads without writing. Returns 202 with the created run (re-read post-orchestration, so a bound-runtime-offline failure is already reflected).
{ "id": "…", "pipelineId": "…", "status": "pending", "phase": "extract", "dryRun": false, "createdAt": "…" }/api/v1/pipelines/{id}/previewcookie sessionRuns the live builder Document over a source sample and returns a per-node trace (rejected rows capped at 20/node). Falls back to the saved spec when spec is omitted.
{ "stream": "public.orders", "limit": 50, "spec": { … } }{
"stream": "public.orders",
"source": { "stream": "public.orders", "fields": [ … ], "rows": [ … ], "truncated": false },
"nodes": [ { "type": "filter", "name": "…", "fields": [ … ], "rows": [ … ],
"rejected": [ { "reasons": [ "amount is null" ], "row": [ … ] } ] } ]
}/api/v1/pipelines/{id}/secretscookie sessionThe secret references the pipeline’s connections need, and whether each is currently satisfied.
[ { "name": "PG_PW", "satisfied": true }, { "name": "S3_KEY", "satisfied": false } ]/api/v1/pipelines/{id}/schedulecookie sessionReturns the schedule, or JSON null when none is set.
{ "pipelineId": "…", "cron": "0 * * * *", "enabled": true, "dryRun": false, "nextRunAt": "…", "lastRunAt": "…" }/api/v1/pipelines/{id}/schedulecookie session · editor+Upserts a 5-field cron schedule; an invalid expression is a 400. When enabled, the response carries the computed nextRunAt.
{ "cron": "0 * * * *", "enabled": true, "dryRun": false }/api/v1/pipelines/{id}/schedulecookie session · editor+Schedule overview
/api/v1/schedulescookie sessionEvery schedule across the workspace (array of scheduleDTO).
/api/v1/schedules/previewcookie sessionValidates a cron string and returns the next 3 fire times. Query: ?cron=0 * * * *.
{ "valid": true, "next": [ "2026-01-02T10:00:00Z", "2026-01-02T11:00:00Z", "2026-01-02T12:00:00Z" ] }Runs, events & artifacts
/api/v1/runscookie sessionFilters (all optional): status, pipelineId, since (RFC3339), q (pipeline name). Default keyset paging (?limit&cursor → nextCursor), or numbered (?page&limit → total).
{ "items": [ { "id": "…", "status": "running", "phase": "load", "rowsRead": 900, … } ], "nextCursor": "…" }/api/v1/runs/{id}cookie sessionexecutedOn is added only on the single-run read — it reports whether the extract ran in the cloud or on a named agent.
{
"id": "…", "pipelineId": "…", "status": "succeeded", "phase": "load",
"rowsRead": 12000, "rowsWritten": 11980, "rowsQuarantined": 20,
"rowsTotal": 12000, "dryRun": false,
"createdAt": "…", "startedAt": "…", "finishedAt": "…",
"executedOn": { "kind": "agent", "name": "office-runtime" }
}/api/v1/runs/{id}/cancelcookie session · editor+Cooperative cancel. Idempotent: an already-finished or unknown run returns cancelling:false rather than an error.
{ "ok": true, "cancelling": true }/api/v1/runs/{id}/eventscookie sessionServer-Sent Events tail of the run log until a terminal state (or ~10 min). Each frame is a runEventDTO.
data: {"id":41,"ts":"…","level":"info","stream":"","message":"Extracted 12000 rows","rowsRead":12000,"rowsWritten":0}/api/v1/runs/{id}/quarantinecookie sessionUp to 200 rejected rows with their reasons and the raw row JSON. Rows can then be approved, discarded, or re-checked.
[ { "id": 1, "stream": "public.orders", "reasons": [ "amount is null" ], "row": { … } } ]/api/v1/runs/{id}/quarantine/approvecookie session · editor+Writes selected quarantined rows into the pipeline’s destination (upsert when the target has a primary key, else append), bypassing the rule that rejected them, and marks each approved. Select by explicit ids or all:true (every still-pending row for the run). edits maps a row id → a partial {column: newValue} patch applied before the write. An agent-bound destination returns 409 (edge approval ships with the runtime); a non-writable connector → 400.
{ "ids": [1, 2], "all": false, "edits": { "1": { "amount": 0 } } }{ "approved": 2, "failed": 0, "errors": [] }/api/v1/runs/{id}/quarantine/discardcookie session · editor+Marks selected rows discarded without writing them. Same { ids | all } selection.
{ "discarded": 2 }/api/v1/runs/{id}/quarantine/revalidatecookie session · editor+Re-checks selected rows (with any edits applied) against the pipeline’s validation rules without writing — reports which now pass. Nothing is mutated.
{ "revalidated": [ { "id": 1, "passes": true, "reasons": [] } ] }/api/v1/runs/{id}/artifactscookie sessionLists staged blob artifacts for the run. No blob backend → 503; no staged data → 404.
{
"streams": [
{
"stream": "public.orders", "rows": 12000, "bytes": 900000, "parts": 2,
"cursorField": "updated_at",
"partList": [ { "n": 1, "rows": 6000, "bytes": 450000, "minCursor": "…", "maxCursor": "…" } ]
}
]
}/api/v1/runs/{id}/artifacts/{stream}cookie sessionStreams one staged stream as decompressed JSONL (application/x-ndjson, attachment). Optional ?part=N (1-based) for a single part; an out-of-range part is 400, a missing manifest 404.
/api/v1/runs/{id}/artifactscookie session · editor+Deletes the run’s staged data. A still-active run → 409; no artifacts → 204 (idempotent).
/api/v1/artifactscookie session · editor+Purges staged artifacts for every finished run in the workspace.
{ "deleted": 7 }Secrets
Named cloud secrets referenced from connection config as ${secret.NAME}. Values are encrypted at rest and never returned. Writes require the store to be configured (SETU_SECRET_KEY).
/api/v1/secretscookie session[ { "name": "PG_PW", "updatedAt": "…" } ]/api/v1/secrets/{name}cookie session · editor+Name must match ^[A-Za-z_][A-Za-z0-9_]*$. No secret store configured → 412. Returns 204.
{ "value": "s3cr3t" }/api/v1/secrets/{name}cookie session · editor+Unknown name → 404.
Notifications, search & overview
/api/v1/notificationscookie sessionThe caller’s notifications (?limit, default 50, max 200).
[ { "id": "…", "type": "schedule.skipped", "title": "…", "body": "…", "payload": {}, "readAt": null, "createdAt": "…" } ]/api/v1/notifications/unread-countcookie session{ "count": 3 }/api/v1/notifications/readcookie sessionMarks all as read. 204.
/api/v1/notifications/{id}/readcookie sessionIdempotent per-notification mark; unknown/other-user id still returns 204.
/api/v1/notifications/streamcookie sessionSSE of the caller’s new notifications (: ping heartbeat every 20s).
/api/v1/searchcookie sessionCross-entity search over datasets, pipelines, connections, and runs (5 hits per group). Query: ?q.
{ "datasets": [ { "id": "…", "label": "Orders", "sublabel": "table" } ], "pipelines": [ … ], "connections": [ … ], "runs": [ … ] }/api/v1/overviewcookie sessionDashboard KPIs, deltas, outcomes, throughput, runs-per-day, and top pipelines. Window: ?range=24h|7d|30d|all (default 7d) or ?from&to.
{
"range": "7d",
"kpis": { "rowsMoved": 120000, "rowsRead": 121000, "quarantined": 40,
"finished": 12, "successRate": 92, "quality": 99, "avgDurationSec": 8.3 },
"outcomes": { "succeeded": 11, "failed": 1, "cancelled": 0 },
"topPipelines": [ { "id": "…", "name": "Orders → Warehouse", "rows": 90000 } ]
}/api/v1/eventscookie sessionWorkspace-wide SSE activity stream of run changes (: connected, then data: frames; : ping every 20s).
Agents (console-side)
These manage edge runtimes from the console. The runtime’s own outbound protocol lives under /agent/v1 below.
/api/v1/agentscookie session[ { "id": "…", "name": "office-runtime", "status": "active", "version": "0.1.0", "lastSeenAt": "…", "createdAt": "…" } ]/api/v1/agentscookie session · editor+{ "name": "office-runtime" }{ "id": "…", "name": "office-runtime", "status": "enrolling", "version": "", "createdAt": "…" }/api/v1/agents/{id}/enroll-tokencookie session · editor+Mints a one-time, 15-minute enrollment token. Only its hash is stored; the plaintext is returned once.
{
"token": "…",
"expiresAt": "…",
"installCommand": "setu enroll <token>",
"controlPlaneUrl": "https://setu.example.com"
}/api/v1/agents/{id}/test-connectioncookie session · editor+Enqueues a test job on the runtime; poll the returned job id. An offline runtime → 409 runtime is not online.
{ "provider": "postgres", "config": { "host": "…", "password": "${secret.PG_PW}" }, "role": "source" }{ "jobId": "…" }/api/v1/agents/{id}cookie session · editor+Soft-revokes the agent — its key stops authenticating. 204.
/api/v1/agent-jobs/{id}cookie sessionPoll an agent job’s status/result (e.g. the async connection test). Workspace-scoped; unknown → 404.
{ "id": "…", "kind": "test", "status": "succeeded", "result": { "ok": true } }Agent protocol · /agent/v1
A separate trust boundary: edge runtimes authenticate by agent key, never a session. enroll is the only public route (an agent has no key until it exchanges an enrollment token); every other route sits behind agent auth and is scoped to the calling agent (jobs it doesn’t own return 404). This is the outbound-only long-poll protocol — see Long-polling & leasing.
/agent/v1/enrollnoneExchanges a one-time enrollment token for a permanent agent key (returned once; only its hash is stored). An invalid/expired/replayed token → 401, no token consumed.
{ "token": "…", "version": "0.1.0" }{ "agentId": "…", "agentKey": "…", "baseUrl": "https://setu.example.com" }/agent/v1/jobs/next?wait={sec}agent keyLong-poll for the next job (lease = 60s). 200 with a leased job, or 204 when the wait window (capped at 30s) closes.
{ "id": "…", "kind": "extract", "runId": "…", "connectionId": "…", "payload": { … } }/agent/v1/jobs/{id}/extract-specagent keySelf-contained extract instructions for an extract job. Config carries ${secret.X} refs intact — never resolved cloud-side. Wrong kind → 404.
{
"runId": "…", "blobPrefix": "ws/…/pl/…/run/…/",
"provider": "postgres", "config": { … },
"streams": [ "public.orders" ], "blob": { … }
}/agent/v1/jobs/{id}/load-specagent keyLoad instructions for a load job, including the write mode and the compiled spec.
{
"runId": "…", "blobPrefix": "ws/…/pl/…/run/…/",
"provider": "postgres", "config": { … },
"mode": "append", "spec": { … }, "blob": { … }
}/agent/v1/jobs/{id}/heartbeatagent keyRenews the lease while working. Always 204 (a renewal on a no-longer-leased job is a harmless no-op).
/agent/v1/jobs/{id}/resultagent keystatus must be succeeded or failed. Idempotent completion; returns 204.
{ "status": "succeeded", "result": { "blobPrefix": "ws/…/run/…/", "rows": 12000 } }{ "status": "failed", "error": "connect: connection refused" }Internal & operational
Root-level routes, outside both API groups.
/internal/scheduler/tickmachine tokenForces one scheduling pass — an external heartbeat that wakes a sleepy free-tier host so due schedules still fire. Bearer SETU_TICK_TOKEN (constant-time compared); when that env is unset the route 404s so it’s never left open. Returns 204.
/healthznone{ "status": "ok" }/readyznoneReadiness — pings the DB pool (2s timeout). 200 {"status":"ready"} or 503 {"status":"unavailable"}.
/metricsnonePrometheus metrics.
/api/v1 over a cookie, gated by role (reads open to members, mutations at editor+, admin actions at admin+); edge runtimes talk /agent/v1 over an agent key, outbound-only, each scoped to itself. Secrets never cross either plane in plaintext — connection config carries ${secret.NAME} references that resolve in the cloud vault or on the edge, wherever the connection is placed.