Skip to main content
The base path is the fixed constant /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 unless apps: 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 unless automations: 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

With createVendo({ 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 code is claimed the moment it is presented, before its verifier is checked. A code_verifier that does not match answers invalid_grant and PKCE verification failed, and retrying the same code then answers invalid_grant and Authorization code is invalid or expired. Codes live 60 seconds.
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 over id.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 require Content-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.