/api/vendo. Bodies are core types encoded as JSON; conversation turns stream over SSE.
Most routes resolve a principal through principal(req). Three surfaces never call it, because each authenticates its own way.
Threads
Files
Approvals and grants
During the resume window a pending answer may omit
request. That still means working, so a poller should keep polling rather than treat it as expired.
Denying an automation’s standing-grant ask also disarms that automation inside the same decision.
Apps
Mounted unlessapps: false.
/apps/seed, /apps/placements, /apps/import, and /apps/review-queue are registered ahead of /apps/:id/*, so none of those words is ever read as an app id.
Automations and runs
Mounted unlessautomations: false.
:id is the automation’s id (atm_…). There is no trigger segment: an
automation is one record, not an app with a list.
Connections and channels
There is no wire-hosted connector OAuth callback. The broker owns the redirect, the host supplies its own
callbackUrl on initiate, and the client polls GET /connections/:id.
Activity, status, and orgs
blocks reports store, agent, actions, guard, apps, automations, plus sandbox ("e2b", "cloud", "custom", or false), model ("custom" or "ladder"), mcp ("local", "broker", or false), and connections ("byo", "cloud", or false).
Organizations are a Vendo Cloud capability. The /orgs family refuses before any principal resolution, and an ?org=<orgId> query parameter or body.org on /approvals or /grants refuses the same way.
Development routes
These answer only in a development composition (createVendo({ development }), which NODE_ENV=development enables). Every other deployment answers 404 because the route is not in the table.
/sync/impact takes no principal and its counts are deployment-wide, which is exactly why a deployment that did not opt in must not carry it.
Doctor routes
These answer on a running dev server, for anything that wants to check the composition over HTTP.vendo doctor does not call them. None takes a principal, so mounting is the whole access control.
/doctor/base-url is the deliberate exception. It reveals only whether VENDO_BASE_URL is set, and production is exactly where that answer matters.
MCP door
WithcreateVendo({ mcp: true, oauth }), the door serves its transport under the wire plus six origin-root discovery documents through the same handler. Bodies follow the MCP transport spec, not the wire’s JSON envelope.
Both well-known OAuth paths are also served at their base-path-prefixed spelling. The set is matched as six exact paths, never as a prefix, so a host’s own OAuth metadata is never shadowed.
/authorize requires PKCE, and S256 is the only method it accepts. A missing code_challenge, one that is not a 43-character base64url digest, or any code_challenge_method other than the literal S256 — including an omitted one — all redirect back with invalid_request and PKCE with code_challenge_method=S256 is required. RFC 8414 metadata advertises exactly that: code_challenge_methods_supported: ["S256"].
A revocation that lands, including one naming a token that no longer exists, answers an empty 200. Five paths answer 400 with an OAuth { error, error_description } body instead: a non-form content type, a missing token or client_id, an unresolvable client_id, a token issued to a different client, and a store whose records do not support atomic claims.
With remoteAs configured, /authorize, /token, /revoke, /register, and the RFC 8414 metadata all answer 404. The external issuer owns those endpoints and RFC 9728 metadata advertises it.
The transport and /connect are unaffected by remoteAs. Delegating the authorization server does not take away the door’s own setup page.
Door paths bypass the wire’s principal resolver and the CSRF JSON gate. The door re-resolves oauth.principal(subject) on every bearer-authenticated transport request, so returning null revokes live sessions.
Webhook verification
Every source registers verification during wiring. Connector sources use their own signed headers; self-minted sources use HMAC-SHA256 overid.timestamp.rawBody.
Signature, timestamp, and delivery id ride the webhook-signature, webhook-timestamp, and webhook-id headers. The accepted clock window is five minutes, the body cap is 1 MiB, and delivery ids are deduplicated.
A verification failure returns 401, resolves no principal, starts no run, and writes one audit event. The secret never appears in a URL.
CSRF floor
Cookie-authenticated state-changing requests requireContent-Type: application/json. The exceptions are exactly four, and each one proves itself another way.
MCP door paths sit outside both rules. They speak MCP and OAuth response shapes, not the wire envelope.
Error envelope
Every non-2xx response has this shape:
Each code’s symptom and fix has its own troubleshooting page.