Skip to content

Reference

Changelog

What changed in the API contract, newest first. Additive changes ship inside /v1; breaking changes never do.

Notable changes to the Tobie External API contract: REST under https://api.tobie.team/v1, MCP at https://api.tobie.team/mcp, the webhook payloads and the published documents (OpenAPI, agent guide, llms.txt). The format follows Keep a Changelog 1.1.0. Versions are the contract version, API_VERSION in packages/api-core/src/version.ts.

Versioning and X-Tobie-Api-Version#

  • The path carries the major version (/v1). Additive changes do not change it: a new operation, a new optional input field, a new output field, a new value in an enum documented as open, a new webhook event type, a new scope. Clients must ignore fields they do not know.
  • Every response, errors included, carries X-Tobie-Api-Version: <major>.<minor> (contract 1.0.0 sends 1.0). The minor rises with each release that adds to the contract; a patch release changes no shape and is not visible in the header. Log the header with any report of unexpected behaviour: it names the contract the server answered under.
  • GET /v1/meta (MCP tool tobie.meta.get) returns the contract version, the versions in service with their sunset dates, the active deprecations, the MCP protocol versions and the caller's effective scopes and client scope.
  • A breaking change is never made inside /v1. Removing or renaming an operation, field, scope or enum value, tightening validation of an existing input, or changing a status or problem code ships as /v2 beside /v1.

Deprecation policy#

  • When /v2 ships, /v1 stays in service, carrying Deprecation and Sunset, for at least six months.
  • A deprecated version or operation answers with Deprecation: @<unix seconds> (RFC 9745, when it became deprecated), Sunset: <HTTP-date> (RFC 8594, when it stops answering; never earlier than the deprecation) and two Link headers: the notice with rel="deprecation" and the replacement with rel="successor-version".
  • Every deprecation is listed in GET /v1/meta (deprecations, and versions[].sunset) and in this file: under "Deprecated" in the release that announces it, with its sunset date, and under "Removed" in the release that withdraws it.
  • Enum values documented as closed do not change within a major version.
  • Nothing is deprecated in 1.0.0: deprecations in GET /v1/meta is empty.

[Unreleased]#

Server-side changes made after the 1.0.0 contract was drawn up. None changes a /v1 request or response shape; the one visible change is in the MCP discovery document below.

Changed#

  • The developer portal on https://api.tobie.team now needs a Tobie account for anything generated from the API registry or that connects an app (WP-API-847, 2026-10-16). Signed-in only: /connect and its guides, every /docs/<guide> page (the /docs overview stays public), /reference and everything under it, and the documents /openapi.json, /llms-full.txt, /agent-guide.md and /search-index.json. A browser without a session is sent to /console/sign-in with the page as next; a program gets a 401 problem document (type unauthorized) whose sign_in and request_access members say how to get in. /v1/openapi.json and /v1/agent-guide.md answer to a bearer credential (an API key or OAuth token), with no scope required, like GET /v1/meta. Unchanged: /llms.txt, /.well-known/**, /health, every /v1 operation and /mcp. The homepage, the audience pages, /security, /status, /changelog and /problems stay public. Anyone can ask for access at https://api.tobie.team/request-access; requests are reviewed by hand.
  • /.well-known/oauth-protected-resource and its /mcp variant: scopes_supported now lists the authorization server's scopes a sign-in requests (openid, email, profile, offline_access) instead of Tobie's API scopes. Hosted MCP clients (ChatGPT's agent connector, 2026-10-14) copy that list into the authorization request, and Supabase Auth refuses any Tobie scope there (invalid_request: unsupported scope), so every sign-in died before the consent page unless the connector's default scopes were cleared by hand. The Tobie scopes the MCP surface uses are still published, under the extension parameter tobie_scopes_supported (same derivation, same canonical order); they are consented on the grant, never requested, and GET /v1/meta reports what a credential actually holds.
  • Tasks visibility for operator principals (migration 20261014230000, OPS-003 G27): an operator principal now reads internal tasks (client_id null) whatever its client scope, an empty scope included, through tasks.list, tasks.get, tasks.timeline, actions.list and search (the index view is SECURITY INVOKER over tasks), and the comments of such a task through tasks.comments.list. This is the dashboard's rule that every active operator sees the board. Measured on production on 2026-10-15: an operator with no client assignments read 0 of 71 tasks, all 71 internal. A client_admin principal and a service_account key are not widened: an internal task stays invisible to both. task_assignees and task_attachments keep their own tenant test, so an internal task read by a scoped operator carries its owner alone in assigneeIds and no attachments (follow-up; the assignees are fixed by G39 under Fixed). No request or response shape changes.
  • CRM and pipeline full coverage for operator principals (migration 20261015010000, OPS-003 G28, Josh's decision of 2026-10-15): an operator principal now reads every row, every client's and the internal ones, whatever its client scope, through crm.leads.list, crm.accounts.list, crm.contacts.list, crm.contacts.search, crm.opportunities.list, pipeline.runs.list, pipeline.runs.get and pipeline.events.list; on those reads a clientId filter outside the token's scope is served instead of answered 404. This is the dashboard's rule that every active operator sees the whole CRM and pipeline. Measured on production on 2026-10-15: an operator with no client assignments read 0 rows on all seven CRM and pipeline tables (unscoped: 359 accounts, 97 opportunities, 366 leads, 365 contacts, 777 touchpoints, 2 pipeline runs, 123 pipeline events). A client_admin principal and a service_account key are not widened (a key keeps its scope and its 404); every other resource keeps the client scope; contact PII stays redacted without pii:read; writes are unchanged. No request or response shape changes; GET /v1/meta still reports the token's client scope.
  • Internal task writes for operator principals (migration 20261016000000, OPS-003 G36): an operator principal now creates, updates, completes and comments on internal tasks (client_id null) whatever its client scope, an empty scope included, through tasks.create, tasks.update, tasks.complete, tasks.comment and tasks.batch_create; the write twin of G27. tasks.create accepts a body with neither projectId nor clientId and creates an internal (team-board) task, which only an operator principal may do; a service_account key and a client_admin get 404 as before, and client-bound tasks keep the client scope. Additive: a body that was refused with validation_failed for lacking both ids is now accepted for an operator. Co-assignees of an internal task are not stored for a scoped principal (api.set_task_assignees); the owner is (fixed by G39 under Fixed).

Fixed#

  • Co-assignees of an internal task for an operator principal (migration 20261016100000, OPS-003 G39): tasks.create and tasks.update now store assigneeIds on an internal task (client_id null) for an operator principal whatever its client scope, an empty scope included, and tasks.list and tasks.get return them, with the assigneeId filter matching a co-assignee. Before, api.set_task_assignees refused a NULL-client task for a scoped principal and the task_assignees policy hid its rows, so the stored set and the read set were the owner alone (the write still succeeded and answered what was stored). The rule is G27's and G36's: a service_account key and a client_admin are not widened, and client-bound tasks keep the client scope. No request or response shape changes. task_attachments keeps its tenant test.
  • tasks.get and tasks.comments.list open an internal task for an operator principal with a client scope (OPS-003 G33). G27 above let the database and the host read return the row, but the runtime's single-resource check still answered 404 for any task with no client unless the principal was unscoped, so an operator with no client assignments saw an internal task in tasks.list and then got not_found opening it or its comments. The check now passes an internal task for the operator kind only; a client-bound task still needs its client in scope, and a service_account key or a client_admin still gets 404 for an internal task. Found 2026-10-15 when the Chief of Staff agent's afternoon run could not read the comments of the internal task "Review current 1099 agreement".
  • An operator whose operators.role is viewer or admin (the dashboard's own vocabulary, not an admin_role label) no longer crashes every authenticated request with invalid input value for enum admin_role. owner, admin and operator read the staff (operator) role_permissions row; viewer holds no permission, so pii:read never reaches it; a client-admin membership role is read as itself when it is a label and holds nothing otherwise. Found 2026-10-14 when bots@tobie.team, invited as a Tobie viewer, signed in from ChatGPT and listed 0 tools.
  • The execution-token minter accepts a signing JWK that carries the advisory members key_ops and use. WebCrypto refuses a private ECDSA import whose key_ops names verify ("Unsupported key usage for a ECDSA key"), so with such a key every read that reaches the database failed inside a 200 JSON-RPC result while tobie.meta.get worked. The minter now imports the key material alone and states its one usage, sign. Found 2026-10-15 on the first authenticated attention brief.

Security#

  • MCP command tools obey API_WRITES_ENABLED. Before, only the REST command routes checked the write switch, so over MCP a token holding a write scope could write while REST answered 404. With the switch off every command tool now answers not_found; read tools are unaffected.
  • OAuth audience tightening (RISK-004, WP-API-503). The token verifier has a switch, API_AUD_LEGACY_MODE: accept (the default, today's behaviour) or reject. In reject mode an OAuth access token whose only aud is authenticated is refused with 401 invalid_token; tokens whose aud is https://api.tobie.team/mcp or https://api.tobie.team, and API keys, are unaffected. Only a token minted before the custom access token hook was enabled is affected; the fix for an agent is to reconnect (authorize again).
  • Token verification fails cleanly when Supabase's key set misbehaves. A JWKS fetch that hangs is abandoned after 5 s, a key set without a usable signing key never replaces a good one, a failing key endpoint is retried at most every 30 s while a cached set is still served, and with no key cached the answer is 503 not_configured with Retry-After: 30 rather than a hung request or a 401.

Closing RISK-004 (operator steps, production)#

  1. Enable the custom access token hook (docs/external-api/GO_LIVE.md step 2.8). From then on new OAuth access tokens carry aud = https://api.tobie.team/mcp; dashboard sessions are untouched.
  2. Connect one OAuth client afresh and decode the access token's payload locally (never paste the token anywhere): aud must be https://api.tobie.team/mcp.
  3. Let the legacy tokens drain: at least one access-token lifetime (jwt_expiry, 3600 s), and until the daily API_AUD_LEGACY_COUNT audit row written by the api-housekeeping cron reports count: 0 for a full 24-hour window.
  4. Set API_AUD_LEGACY_MODE=reject on the pm-dashboard production environment and redeploy (the host reads it once its wiring lands; until then the verifier keeps the accept default).
  5. Verify: API_REQUEST audit rows with payload->>'aud_legacy' = 'true' stop appearing, and any stale token shows up as payload->>'auth_reason' = 'aud_legacy_rejected' with status 401.
  6. Allowlist audit: API_OAUTH_CLIENT_MODE is allowlist in production; every OAuth client registered in Supabase Auth either has a row in api_oauth_client_policies with a deliberate scope_ceiling or is deleted. Record client ids and names only.
  7. Record the evidence on the WP-API-503 row and mark RISK-004 CLOSED (roadmap Part 9.3).

Rollback: set API_AUD_LEGACY_MODE=accept (or remove it) and redeploy. Disabling the hook re-opens the transition, so switch to accept before disabling it.

[1.0.0] - unreleased#

The first public contract. It is released the day /v1 goes live in production (docs/external-api/GO_LIVE.md); writes, MCP, webhooks and the event stream belong to this contract and are switched on by flag, not by a version change.

Added#

  • Credentials: API keys (tobie_sk_live_..., tobie_sk_test_...) acting for the operator who owns them, and OAuth 2.1 access tokens for operators and client admins. Scopes are resource:verb; a credential's effective scopes are the intersection of what it was granted and what its owner's live role allows.
  • Reads, and commands in tiers: Tier 1 executes, Tier 2 and 3 return 202 Accepted with a proposal a human approves. Prefer: dry-run returns the plan without writing; Idempotency-Key makes a retry replay the first answer (Idempotency-Replayed: true); If-Match with the ETag refuses a lost update.
  • Cursor pagination (items, nextCursor), RFC 9457 problem details, rate-limit headers (RateLimit-Policy, RateLimit, X-RateLimit-*), X-Request-Id and traceparent.
  • The MCP server, generated from the same registry, with the same scopes and approvals.
  • Long-running work as operations, the events feed and its Server-Sent Events stream, search, and file upload and download URLs; webhooks signed per the Standard Webhooks scheme, registered and managed at /webhook-endpoints (a secret shown once, rotation with a 24-hour overlap, a synchronous :test, delivery history and replay).
  • Documents: OpenAPI 3.1 (/v1/openapi.json), the interactive reference (/v1/docs), the agent guide (/v1/agent-guide.md) and /llms.txt; the TypeScript client in packages/api-client.

Resources and operations#

Contract version 1.0.0, sent as X-Tobie-Api-Version: 1.0 on every response. 82 operations (43 reads, 39 commands) in 23 families, 54 of them also MCP tools; 32 scopes; 10 webhook event types. Routes, inputs and outputs: the agent guide and the OpenAPI document.

FamilyReadsCommands (tier)Scopes
metameta.getnoneany credential
clientsclients.list, clients.get, clients.summary, clients.health, clients.launch_readiness, clients.memberships.listclients.publish_hub (T2)clients:read, memberships:manage, reviews:release
projectsprojects.list, projects.getprojects.create (T2), projects.update (T1), projects.stage.advance (T2)projects:read, projects:write
taskstasks.list, tasks.get, tasks.comments.listtasks.create (T1), tasks.update (T1), tasks.complete (T1), tasks.comment (T1), tasks.batch_create (T1)tasks:read, tasks:write
timelinetimeline.listnonetasks:read
actionsactions.listnoneactions:read
notificationsnotifications.listnotifications.read (T1)notifications:read
intakeintake.list, intake.getintake.create (T2), intake.draft.set_field (T1), intake.reopen_field (T2), intake.finalize (T2)intake:read, intake:write
extractionextraction.getnoneextraction:read
reviewsreviews.list, reviews.getreviews.release (T2)reviews:read, reviews:release
pipelinepipeline.runs.list, pipeline.runs.get, pipeline.events.listpipeline.run.start (T2), pipeline.run.rerun_partial (T1)pipeline:read, pipeline:run
crmcrm.leads.list, crm.accounts.list, crm.contacts.list, crm.contacts.search, crm.opportunities.listcrm.lead.create (T1), crm.touchpoint.log (T1), crm.opportunity.convert (T2)crm:read, crm:write
campaignscampaigns.listcampaigns.propose_send (T2)campaigns:read, campaigns:propose
filesfiles.download_urlfiles.upload_url (T1), files.register (T1)files:read, files:write
searchsearch.querynonesearch:read
approvalsapprovals.list, approvals.getapprovals.approve (T1), approvals.reject (T1)approvals:read, approvals:decide
operationsoperations.list, operations.getoperations.cancel (T1)operations:read
eventsevents.list, events.streamnoneevents:read
webhookswebhooks.endpoints.list, webhooks.endpoints.get, webhooks.deliveries.listwebhooks.endpoint.create (T1), webhooks.endpoint.update (T1), webhooks.endpoint.delete (T1), webhooks.endpoint.rotate_secret (T1), webhooks.endpoint.test (T1), webhooks.delivery.replay (T1)webhooks:manage
oauth_grantsoauth_grants.listoauth_grants.revoke (T1)any credential
keyskeys.listkeys.create (T2), keys.rotate (T2), keys.revoke (T1)keys:manage
membershipsnonememberships.invite (T2), memberships.revoke (T2)memberships:manage
executionexecution.jobs.getexecution.requests.propose (T1)execution:read, execution:propose

Webhook event types: task.created, task.updated, task.completed, project.stage_advanced, intake.finalized, review.released, membership.invited, membership.revoked, approval.approved, approval.rejected.