{"openapi":"3.1.0","info":{"title":"Elaichi API","version":"0.1.0","description":"Control-plane HTTP API for Elaichi — the MCP control plane for SaaS teams. Every operation here is callable with an organization API token (or, on the MCP and OAuth surfaces, per the relevant protocol). Endpoints reached only by the Elaichi web app with a session cookie, by a browser redirect, by an inbound webhook, or by platform staff are deliberately not documented."},"servers":[{"url":"https://api.elaichi.ai","description":"Production"}],"tags":[{"name":"Health"},{"name":"Schema"},{"name":"Auth"},{"name":"Current user"},{"name":"Organizations"},{"name":"Entitlements"},{"name":"Permissions"},{"name":"Roles"},{"name":"Teams"},{"name":"Members"},{"name":"Invites"},{"name":"Org domains"},{"name":"API tokens"},{"name":"Audit log"},{"name":"Connectors"},{"name":"Custom connectors"},{"name":"Connections"},{"name":"Templates"},{"name":"Toolboxes"},{"name":"Synthetic tools"},{"name":"MCP control plane"},{"name":"MCP OAuth"},{"name":"OAuth discovery"},{"name":"Restrictions"},{"name":"Access requests"},{"name":"Logging destinations"},{"name":"Notification destinations"},{"name":"SSO connections"},{"name":"SCIM tokens"},{"name":"SCIM groups"},{"name":"SCIM v2"},{"name":"Group mapping"}],"paths":{"/health":{"get":{"operationId":"getHealth","summary":"Health check","tags":["Health"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"service":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Public liveness probe. Touches no storage, so a 200 means the worker is up, not that D1 is.","security":[]}},"/schema/openapi.json":{"get":{"operationId":"getOpenApiJson","summary":"This OpenAPI document (JSON)","tags":["Schema"],"responses":{"200":{"description":"OpenAPI 3.1 document","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The live specification for this deployment, public and unauthenticated. The OAuth discovery documents point here as their documentation URL.","security":[]}},"/schema/openapi.yml":{"get":{"operationId":"getOpenApiYaml","summary":"This OpenAPI document (YAML)","tags":["Schema"],"responses":{"200":{"description":"OpenAPI 3.1 document.","content":{"application/yaml":{"schema":{"type":"string"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The same document as `/schema/openapi.json`, served as YAML.","security":[]}},"/openapi.json":{"get":{"operationId":"getRootOpenApiJson","summary":"This OpenAPI document (JSON, root alias)","tags":["Schema"],"responses":{"200":{"description":"OpenAPI 3.1 document","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Identical to `GET /schema/openapi.json`. Exists at this conventional path for tooling that looks for an OpenAPI document at the API origin without being told where it lives.","security":[]}},"/openapi.yaml":{"get":{"operationId":"getRootOpenApiYaml","summary":"This OpenAPI document (YAML, root alias)","tags":["Schema"],"responses":{"200":{"description":"OpenAPI 3.1 document.","content":{"application/yaml":{"schema":{"type":"string"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Identical to `GET /schema/openapi.yml`, at the conventional root path.","security":[]}},"/auth/saml/{connectionId}/metadata":{"get":{"operationId":"samlMetadata","summary":"SAML SP metadata","tags":["Auth"],"responses":{"200":{"description":"SP metadata XML.","content":{"application/xml":{"schema":{"type":"string"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Service-provider metadata XML to hand to the IdP administrator when configuring a SAML connection created with `POST /sso-connection`. Public — no credential.","security":[],"parameters":[{"name":"connectionId","in":"path","required":true,"schema":{"type":"string"},"description":"SSO connection id (`sso_…`)."}]}},"/user/me":{"get":{"operationId":"getCurrentUser","summary":"Current user bootstrap payload","tags":["Current user"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/User"},{"type":"object","properties":{"mfa_enabled":{"type":"boolean","description":"True when TOTP is active on this account."},"is_root_org_staff":{"type":"boolean","description":"Platform staff who are also a member of the root organization — gates the staff console."},"organizations":{"type":"array","description":"One entry per active membership. Organizations where membership has lapsed are omitted.","items":{"type":"object","properties":{"organization":{"$ref":"#/components/schemas/Organization"},"member":{"type":"object","description":"Membership facts only — roles and teams are the sibling fields below, not nested here.","properties":{"user_id":{"type":"string","description":"User id (`usr_…`)."},"status":{"type":"string"},"via":{"type":["string","null"],"description":"How they joined, e.g. invite or verified domain."},"joined_at":{"type":"string","format":"date-time"}}},"roles":{"type":"array","description":"Role summaries held in this organization.","items":{"type":"object","properties":{"id":{"type":"string","description":"Role id (`role_…`)."},"name":{"type":"string"},"is_system":{"type":"boolean"},"seat_class":{"type":"string","enum":["billable","free_admin"]}}}},"teams":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":"string"}}}},"permissions":{"type":"array","items":{"type":"string"},"description":"Effective permission names, unioned across roles. This is the authoritative list for deciding what to show; the server re-checks it on every request."}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The profile of whoever the credential belongs to, plus one entry per organization they are an active member of, each carrying that org's roles, teams and effective permissions. Call this first: it is how a client learns which organization ids it may put in `X-Organization-Id` and which actions to offer. With an org API token this still returns the token owner's memberships, so read `organizations` rather than assuming the token's org is the only one.","security":[{"bearerAuth":[]}]}},"/organization":{"post":{"operationId":"createOrganization","summary":"Create organization","tags":["Organizations"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Creates an organization and makes the caller its first member, holding the immutable Org Owner role; the predefined role set is seeded at the same time. Requires an interactive session — an API token is refused with 403, since a token scoped to one org must not be able to create another. Also requires this deployment to have public signup enabled (`PUBLIC_SIGNUP`), otherwise 403 `signup_disabled`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{0,58}[a-z0-9]$","description":"Lowercase alphanumeric with hyphens. Generated from the name when omitted."},"region":{"type":"string","enum":["us","eu","apac"],"default":"us","description":"Data location, fixed at creation: `eu`/`us` pin a hard Durable Object jurisdiction, `apac` is a placement hint. It cannot be changed afterwards."}}}}}}},"get":{"operationId":"listOrganizations","summary":"List your organizations","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"$ref":"#/components/schemas/Organization"}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Every organization the calling user belongs to. Cursor-paginated like any other list endpoint — follow `next_cursor` to reach the rest rather than assuming one response is the whole set. With an API token, scoped to exactly one org, the result is always a single-item page with `next_cursor: null`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]}},"/organization/{id}":{"get":{"operationId":"getOrganization","summary":"Get organization","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Membership is required. The org comes from the path, so no `X-Organization-Id` header is needed.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]},"patch":{"operationId":"patchOrganization","summary":"Update organization","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires the `org:manage` permission and an active Gold or Black subscription. **`settings` is REPLACED wholesale, not merged**: whatever object you send becomes the entire settings object, so any key you omit is deleted. Read the current organization first and send the merged result. `name` is a normal field update, and omitting a field leaves it untouched. `slug`, `plan` and `region` cannot be changed here.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"settings":{"type":"object","additionalProperties":true,"description":"Replaces the stored settings object entirely. Omitted keys are lost."}}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]},"delete":{"operationId":"deleteOrganization","summary":"Delete organization","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]},"organization_id":{"type":"string"},"pending_deletion":{"type":"boolean","enum":[true],"description":"Always `true` here: this route only schedules. The organization is read-only until `purge_after` and can be brought back with `POST /organization/{id}/restore`."},"purge_after":{"type":"string","format":"date-time","description":"When the daily cron will run the full teardown, 30 days out. The restore window closes at this instant. A repeat call on an already-scheduled organization returns the deadline already set, never a later one."},"summary":{"type":"object","description":"Only what THIS call severed. Counts are of items successfully removed. The purge-only counts (`api_tokens_revoked`, `conversations_purged`, `logo_objects_deleted`) are absent here, not zero: they appear only on the day-30 purge and on a staff `immediate` delete.","properties":{"connections_deleted":{"type":"integer"},"scim_tokens_revoked":{"type":"integer"},"oauth_grants_revoked":{"type":"integer"},"stripe_subscription_cancelled":{"type":"boolean"},"failures":{"type":"array","description":"Steps that did not come off cleanly on this run, one entry per step. Empty on a clean run; a retry of the same delete may clear them. Bounded by the fixed number of teardown steps, never by organization size — a step that walks a collection reports a count and a few sample ids in its single entry rather than one entry per row.","items":{"type":"object","properties":{"step":{"type":"string"},"detail":{"type":"string"}}}},"residue":{"type":"array","description":"Stores a deletion cannot reach at all. Empty on this route, because scheduling touches none of them; the day-30 purge (and a staff immediate delete) reports them, and that list is never a sign that anything went wrong. It is the list an erasure request is answered with.","items":{"type":"object","properties":{"step":{"type":"string"},"detail":{"type":"string"}}}}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"**Schedules permanent deletion 30 days out — it does not erase the organization today.** What happens IMMEDIATELY and cannot be undone: every connection is severed and its vault credential deleted (a restore returns the organization but NOT the connections — each must be re-authorized by hand), every SCIM token and MCP OAuth grant is revoked, and the Stripe subscription is canceled (immediately, not at period end). Everything else — members, roles, teams, toolboxes, templates, restrictions, conversations, audit history, the logo, API tokens — survives and returns on a restore. For the next 30 days the organization is READ-ONLY: every GET still works (the paginated read APIs are the export path, and existing API tokens keep reading through them, so an export script needs no new credential), and every other method answers `403 organization_pending_deletion` except `POST /organization/{id}/restore` and the billing checkout/portal routes. MCP, tool execution and new connections are refused outright. On day 30 a daily cron runs the full teardown. Calling this twice is idempotent — the second call returns the same `purge_after` with an all-zero summary. Requires the `org:delete` permission, which **only the Org Owner role holds** — an Org Admin is refused with 403 `permission_required`. Requires an interactive session and step-up reauthentication: without a valid `X-Step-Up-Token` the call answers `428 step_up_required` (see the step-up flow). `confirm_slug` must equal the organization’s own `slug` and is re-checked server-side, so a client that skips its confirmation prompt does not skip the check. The platform root organization can never be deleted: `403 root_organization_cannot_be_deleted`. Unlike every other mutation here this does NOT require an active subscription — a locked or expired organization must still be able to leave. On `200` the `summary` describes only what THIS call severed; `api_tokens_revoked`, `conversations_purged` and `logo_objects_deleted` are absent and `residue` stays empty because this phase touches none of them (the day-30 purge reports those). A non-empty `failures` means a credential this call promised to revoke may still be live at the provider, and a client should say so rather than repeat the promise.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["confirm_slug"],"properties":{"confirm_slug":{"type":"string","description":"Must equal this organization’s `slug`. Re-checked server-side."},"reason":{"type":"string","maxLength":500,"description":"Optional note, recorded on the audit entry this deletion writes before it runs."}}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]}},"/organization/{id}/restore":{"post":{"operationId":"restoreOrganization","summary":"Cancel a scheduled deletion","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"restored":{"type":"boolean","enum":[true]},"organization":{"$ref":"#/components/schemas/Organization"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Undoes `DELETE /organization/{id}` inside its 30-day window: the purge is canceled and the organization becomes writable again. Connections do NOT come back — their vault credentials were deleted at the original delete, so each has to be re-authorized by hand. Requires `org:manage`, so an Org Admin can restore and not only the Owner who could delete. No body and no step-up: the delete is the dangerous direction and the undo should not cost the caller a factor. Refuses with `409` when the organization is not currently scheduled for deletion or the window has passed, with `403 organization_blocked` when the organization is under a staff hold, and with `subscription_required` when the plan lapsed — the delete canceled the Stripe subscription, so a restore without a new one is refused rather than handed back an organization every entitlement gate would immediately lock again.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]}},"/organization/{id}/logo":{"put":{"operationId":"putOrganizationLogo","summary":"Upload organization logo","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Raw image body (PNG/JPEG/WebP), max 2 MiB — not multipart. Stores the bytes and returns the organization with `logo` set to the public URL that `GET /public/org-logos/{organizationId}/{objectId}` (or the configured CDN) serves. The previous logo object is deleted only after the new one is stored. Requires `org:manage` and an active subscription.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"image/png":{"schema":{"type":"string","format":"binary"}},"image/jpeg":{"schema":{"type":"string","format":"binary"}},"image/webp":{"schema":{"type":"string","format":"binary"}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]},"delete":{"operationId":"deleteOrganizationLogo","summary":"Remove organization logo","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Deletes the stored image and clears `logo`. Irreversible — the bytes are gone and must be re-uploaded. Requires `org:manage` and an active subscription.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]}},"/organization/{id}/entitlements":{"get":{"operationId":"getOrganizationEntitlements","summary":"Get effective plan and feature map","tags":["Entitlements"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":["string","null"],"enum":["gold","black",null],"description":"Effective plan. `null` means locked — gated routes answer 403 `subscription_required`."},"trial_ends_at":{"type":["string","null"],"format":"date-time"},"trial_active":{"type":"boolean","description":"Local pre-Checkout Gold trial only."},"trial_indefinite":{"type":"boolean","description":"Staff-granted indefinite trial: unlocked with no end date. `trial_active` stays false — there is no clock to count down."},"features":{"type":"object","description":"Feature key → allowed. Every known feature key is present, so a missing key means unknown, not denied.","additionalProperties":{"type":"boolean"}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"What this organization may use right now. Call this before offering a gated feature rather than reading `plan` off the organization: `plan` here is the EFFECTIVE plan (null when locked), which folds in the trial and the Stripe subscription status. Membership required; any member may read it.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]}},"/organization/{id}/billing":{"get":{"operationId":"getOrganizationBilling","summary":"Billing status","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","description":"Stored plan on the row: `gold`, `black`, or `none` once locked."},"effective_plan":{"type":["string","null"],"enum":["gold","black",null],"description":"What the org is actually entitled to right now, after trial and Stripe subscription status are taken into account. `null` means locked — gated features refuse with `subscription_required`."},"trial_ends_at":{"type":["string","null"],"format":"date-time"},"trial_active":{"type":"boolean","description":"The local pre-Checkout Gold trial only. A Stripe `trialing` subscription is not this flag."},"stripe_customer_id":{"type":["string","null"]},"stripe_subscription_id":{"type":["string","null"]},"stripe_subscription_status":{"type":["string","null"]},"stripe_price_id":{"type":["string","null"]},"has_customer":{"type":"boolean"},"billing_configured":{"type":"boolean","description":"False when this deployment has no Stripe key configured."},"seat_count":{"type":"integer","description":"Billable memberships, with the paid-subscription minimum of one. This is the Checkout quantity."},"available_plans":{"type":"object","description":"Which prices this deployment has configured.","properties":{"gold":{"type":"boolean"},"gold_monthly":{"type":"boolean"},"gold_yearly":{"type":"boolean"},"black":{"type":"boolean"}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Subscription and seat state for the plan screen. Requires the `billing:view` permission. For what the org may actually *use*, prefer `GET /organization/{id}/entitlements`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]}},"/organization/{id}/billing/checkout":{"post":{"operationId":"createBillingCheckout","summary":"Create Stripe Checkout session","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Hosted Stripe Checkout URL — send the browser here."},"session_id":{"type":"string"},"seat_count":{"type":"integer","description":"Quantity submitted to Stripe."},"trial_period_days":{"type":"integer","description":"Remaining local trial carried into Stripe; `0` charges now."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Starts a per-seat subscription and returns the hosted Checkout URL to redirect the user to. Quantity is the current billable member count, and any remaining local trial is carried into Stripe as trial days. Requires `billing:manage`. Refuses with `subscription_exists` when a live (or past-due) subscription is already attached — change plans through `POST /organization/{id}/billing/portal` instead. Annual billing is Gold only.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["gold","black"],"default":"gold"},"interval":{"type":"string","enum":["month","year"],"default":"month","description":"Gold only. `year` with `black` is rejected as `interval_unsupported`."}}}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]}},"/organization/{id}/billing/portal":{"post":{"operationId":"createBillingPortal","summary":"Create Stripe Customer Portal session","tags":["Organizations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Returns the portal URL where the customer changes plan, payment method, or cancels. Requires `billing:manage` and an existing Stripe customer — before the first Checkout this answers `no_stripe_customer`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Organization id (`org_…`)."}]}},"/permission":{"get":{"operationId":"listPermissions","summary":"Permission catalog","tags":["Permissions"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Permission name, e.g. `member:manage` — this is what goes on a role."},"label":{"type":"string","description":"Short human label."},"description":{"type":"string"},"group":{"type":"string","description":"Display section for a role editor, e.g. `Connectors`."},"high_trust":{"type":"boolean","description":"True when granting this permission lets its holder create something the organization’s own governance rules cannot bind afterwards — today only `connector:create`, whose connectors are governed by identity and not by the destination they dial (see docs/access-model.md §11). Always present; surface it wherever the permission is granted."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The static catalog of permission names a role may hold, with display copy — the source of truth for the role editor and for validating `permissions` on `POST /role` / `PATCH /role/{id}`. Unpaginated and not org-scoped: the catalog is the same for every organization, and `next_cursor` / `prev_cursor` are always null.","security":[{"bearerAuth":[]}]}},"/role":{"get":{"operationId":"listRoles","summary":"List roles","tags":["Roles"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"$ref":"#/components/schemas/Role"}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Roles in this organization, system and custom alike. Any member may read them — this is what populates a role picker before `PATCH /member/{userId}` or `POST /invite`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"post":{"operationId":"createRole","summary":"Create role","tags":["Roles"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Role"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Authoring custom roles requires the `role:manage` permission and the `custom_roles` plan feature; the predefined system roles stay readable and assignable on every plan. You may only grant permissions you hold yourself — otherwise an admin could mint a role carrying `billing:manage` and assign it to themselves. Validate names against `GET /permission`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","permissions"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":["string","null"],"maxLength":2000},"permissions":{"type":"array","maxItems":100,"items":{"type":"string"},"description":"Permission names from `GET /permission`. The complete set for this role."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/role/{id}":{"get":{"operationId":"getRole","summary":"Get role","tags":["Roles"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Role"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Role id (`role_…`)."}]},"patch":{"operationId":"patchRole","summary":"Update role","tags":["Roles"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Role"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"**`permissions` is REPLACED wholesale, not merged** — the array you send becomes the role's entire permission set, so anything omitted is revoked from every member holding the role. Read the role first and send the full intended list. Omitting `permissions` entirely leaves it untouched. Edits apply live: effective permissions are recomputed per request and every cached permission context for the org is invalidated immediately. Requires `role:manage`, the `custom_roles` feature, and that you hold every permission you are granting.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":["string","null"],"maxLength":2000},"permissions":{"type":"array","maxItems":100,"items":{"type":"string"},"description":"Replaces the role's permission set entirely."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Role id (`role_…`)."}]},"delete":{"operationId":"deleteRole","summary":"Delete role","tags":["Roles"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible, and it takes effect immediately for everyone: members holding this role lose the permissions it carried on their very next request. Check who holds it via `GET /member` first. Requires `role:manage` and the `custom_roles` feature. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Role id (`role_…`)."}]}},"/team":{"get":{"operationId":"listTeams","summary":"List teams","tags":["Teams"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"$ref":"#/components/schemas/Team"}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the team name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]},"post":{"operationId":"createTeam","summary":"Create team","tags":["Teams"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `team:manage`. Team names are unique within the organization.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":["string","null"],"maxLength":2000}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/team/{id}":{"get":{"operationId":"getTeam","summary":"Get team","tags":["Teams"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Team id (`team_…`)."}]},"patch":{"operationId":"patchTeam","summary":"Update team","tags":["Teams"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Merge-patch on `name`/`description`; membership is managed through the `/members` subroutes, never here. Allowed for holders of `team:manage` and for administrators of this particular team.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"description":{"type":["string","null"],"maxLength":2000}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Team id (`team_…`)."}]},"delete":{"operationId":"deleteTeam","summary":"Delete team","tags":["Teams"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible. Removes the team and all of its memberships; members keep their org roles but lose anything granted through this team, including access to team-scoped connections and shares. Requires `team:manage` (a team administrator cannot delete their own team). A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Team id (`team_…`)."}]}},"/team/{id}/member":{"get":{"operationId":"listTeamMembers","summary":"List team members","tags":["Teams"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"$ref":"#/components/schemas/TeamMember"}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The team's roster, cursor-paginated — where a team row's `member_count` leads. A team row carries only a bounded `member_preview`, never the roster itself, so this is how a client reads who is in a team of any size. 404 when the team does not exist, which keeps an empty team distinguishable from a missing one.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Team id (`team_…`)."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the member's name or email, applied server-side. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]}},"/team/{id}/members":{"post":{"operationId":"addTeamMember","summary":"Add team member","tags":["Teams"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Adds one existing org member to the team and returns the whole team. Additive — it does not disturb the other members. Allowed for `team:manage` or an administrator of this team.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id"],"properties":{"user_id":{"type":"string","description":"User id (`usr_…`) of an active org member."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Team id (`team_…`)."}]}},"/team/{id}/members/{userId}":{"patch":{"operationId":"setTeamMemberAdmin","summary":"Appoint or remove a team administrator","tags":["Teams"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Toggles team-admin rights for one member. Team administrators may rename their team and manage its ordinary members, and may manage team-scoped connections. Requires `team:manage` — a team administrator cannot appoint another.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["is_admin"],"properties":{"is_admin":{"type":"boolean"}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Team id (`team_…`)."},{"name":"userId","in":"path","required":true,"schema":{"type":"string"},"description":"User id (`usr_…`) of a team member."}]},"delete":{"operationId":"removeTeamMember","summary":"Remove team member","tags":["Teams"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Team"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Removes one member from the team and returns the team. The member keeps their org membership and roles. A team administrator may remove ordinary members but not another administrator — that needs `team:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Team id (`team_…`)."},{"name":"userId","in":"path","required":true,"schema":{"type":"string"},"description":"User id (`usr_…`)."}]}},"/member":{"get":{"operationId":"listMembers","summary":"List members","tags":["Members"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"$ref":"#/components/schemas/Member"}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Org members, each enriched with the matching global user profile so a directory renders without a second lookup. `user` is null when the profile row is unavailable.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the member's name or email. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]}},"/member/{userId}":{"patch":{"operationId":"patchMember","summary":"Set member role","tags":["Members"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Member"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"**A member holds exactly one role per organization.** `role_ids` must contain exactly one id and REPLACES whatever they held; zero or two or more answers 400 `role_cardinality`. To combine two roles' permissions, author a custom role (`POST /role`) — a second assignment would create a privilege level no role names. Takes effect on the member's next request. Requires `member:manage`, and you may only assign a role whose permissions you hold yourself, so a member admin cannot promote anyone (including themselves) past their own level. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["role_ids"],"properties":{"role_ids":{"type":"array","minItems":1,"maxItems":1,"items":{"type":"string"},"description":"Exactly one role (`role_…`). It replaces the member's current role."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"userId","in":"path","required":true,"schema":{"type":"string"},"description":"User id (`usr_…`) of an org member."}]},"delete":{"operationId":"deleteMember","summary":"Remove member","tags":["Members"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`connections_need_resolution` — shared toolboxes still reference this member's personal connections. `error.details.connections` lists each one; retry with a `connection_actions` entry per connection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible: revokes the member's access to this organization immediately. Re-adding them means a fresh invite, and their roles and team memberships are not restored. Their personal connections are resolved first — call `GET /member/{userId}/offboarding` to see which ones shared toolboxes depend on, then supply a `connection_actions` entry for each. Personal connections with no entry are DELETED, which destroys the vaulted credential. Nothing is applied unless every referenced connection has a resolution: otherwise the call fails with 409 `connections_need_resolution` and lists the unresolved connections in `error.details`. `transfer` hands a connection to one named member and rewrites `owner_user_id` only (§2) — its existing grants are untouched. There is deliberately no \"give it to the team/org\" shortcut: that is a grant, a separate act via `POST /connection/{id}/share`, not something offboarding manufactures on an admin's behalf. Requires `member:manage`. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. The gate is demanded LAST, after every resolution refusal above, so an admin never re-authenticates only to be told the removal needs decisions first.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"connection_actions":{"type":"array","maxItems":200,"description":"One decision per personal connection that a shared toolbox references.","items":{"type":"object","required":["connection_id","action"],"properties":{"connection_id":{"type":"string","description":"Connection id (`conn_…`) from the offboarding preflight."},"action":{"type":"string","enum":["transfer","delete"],"description":"`transfer` keeps the vaulted credential and the toolbox entries that pin the connection working, moving ownership to `target_user_id`; `delete` destroys the credential."},"target_user_id":{"type":"string","description":"Required for `transfer` (`usr_…`) — the member who becomes the connection's new owner. Cannot be the member being removed."}}}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"userId","in":"path","required":true,"schema":{"type":"string"},"description":"User id (`usr_…`) of an org member."}]}},"/member/{userId}/offboarding":{"get":{"operationId":"getMemberOffboarding","summary":"Offboarding preflight for member removal","tags":["Members"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string"},"connections":{"type":"array","description":"Each entry pairs a personal connection with the toolboxes referencing it.","items":{"type":"object","additionalProperties":true}},"delegated_entries":{"type":"array","description":"Non-blocking (§6): entries elsewhere in the org whose delegation chain depends on this member, independent of which connections they own.","items":{"type":"object","properties":{"toolbox_id":{"type":"string"},"toolbox_name":{"type":"string"},"entry_id":{"type":"string"},"connector_slug":{"type":["string","null"]},"tool_name":{"type":["string","null"]},"connection_id":{"type":["string","null"]}}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"What removing this member would break: their personal (user-scoped) connections and, per connection, the toolboxes that reference them (`needs_resolution: true` on a connection means at least one of those toolboxes is visible beyond this member — that connection MUST get a `connection_actions` entry on `DELETE /member/{userId}` or the call refuses). Separately, `delegated_entries` lists entries on OTHER members' toolboxes whose pin currently rides on THIS member's own `use` grant, not their ownership (docs/access-model.md §6) — non-blocking, member removal never refuses over it, but once the member is gone those entries go unmet until somebody with their own `use` on the connection re-pins them; there is no operation that does that automatically. Read-only — call it before `DELETE /member/{userId}` to collect the transfer-or-delete decisions that call requires. Requires `member:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"userId","in":"path","required":true,"schema":{"type":"string"},"description":"User id (`usr_…`) of an org member."}]}},"/invite":{"get":{"operationId":"listInvites","summary":"List pending invites","tags":["Invites"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Invite id (`inv_…`)."},"email":{"type":"string","format":"email","description":"Lowercased at creation."},"role_ids":{"type":"array","items":{"type":"string"},"description":"The single role (`role_…`) the invitee receives on acceptance."},"team_ids":{"type":"array","items":{"type":"string"},"description":"Teams (`team_…`) they are added to."},"status":{"type":"string","description":"Lifecycle state; only `pending` invites can still be accepted."},"expires_at":{"type":"string","format":"date-time","description":"Seven days after creation."},"invited_by":{"type":["string","null"],"description":"User id (`usr_…`) of the inviter."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `member:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"post":{"operationId":"createInvite","summary":"Create invite","tags":["Invites"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Invite id (`inv_…`)."},"email":{"type":"string","format":"email","description":"Lowercased at creation."},"role_ids":{"type":"array","items":{"type":"string"},"description":"The single role (`role_…`) the invitee receives on acceptance."},"team_ids":{"type":"array","items":{"type":"string"},"description":"Teams (`team_…`) they are added to."},"status":{"type":"string","description":"Lifecycle state; only `pending` invites can still be accepted."},"expires_at":{"type":"string","format":"date-time","description":"Seven days after creation."},"invited_by":{"type":["string","null"],"description":"User id (`usr_…`) of the inviter."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","properties":{"token":{"type":"string","description":"Raw invite token — returned once, never retrievable again."},"email_sent":{"type":"boolean","description":"False when the invite email could not be delivered (the token still works)."}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Emails the invitee an accept link and returns the raw `token` once so a console can also show a copy-paste link — it is stored only as a hash and cannot be read back. Valid for seven days. An invite is a deferred role assignment, so the same no-escalation rule as `PATCH /member/{userId}` applies: you may only invite with roles whose permissions you already hold. Requires `member:manage`. Inviting someone who is already a member answers 409. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. Minting a redeemable credential for an address the caller chose is why this one is in the CREDENTIAL class: the ten-minute freshness window never satisfies it.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","role_ids"],"properties":{"email":{"type":"string","format":"email","description":"Lowercased before storage."},"role_ids":{"type":"array","minItems":1,"maxItems":1,"items":{"type":"string"},"description":"Exactly one role (`role_…`), granted on acceptance."},"team_ids":{"type":"array","maxItems":50,"items":{"type":"string"},"description":"Teams (`team_…`) to join. Defaults to none."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/invite/{id}/resend":{"post":{"operationId":"resendInvite","summary":"Resend pending invite","tags":["Invites"],"responses":{"200":{"description":"Resent","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Invite id (`inv_…`)."},"email":{"type":"string","format":"email","description":"Lowercased at creation."},"role_ids":{"type":"array","items":{"type":"string"},"description":"The single role (`role_…`) the invitee receives on acceptance."},"team_ids":{"type":"array","items":{"type":"string"},"description":"Teams (`team_…`) they are added to."},"status":{"type":"string","description":"Lifecycle state; only `pending` invites can still be accepted."},"expires_at":{"type":"string","format":"date-time","description":"Seven days after creation."},"invited_by":{"type":["string","null"],"description":"User id (`usr_…`) of the inviter."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","properties":{"token":{"type":"string","description":"Raw invite token — returned once, never retrievable again."},"email_sent":{"type":"boolean","description":"False when no invite email went out (the token still works)."}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Mints a fresh token and a fresh 7-day expiry for a pending invite, swapped onto the same row — the invite id is unchanged and the previous link stops working. The answer to \"the invite email never arrived\": creating the same invite again is refused as a duplicate (`409`), so this is the route that sends a new link. The raw `token` comes back once, as on create. The caller's own grant ceiling is re-checked against the invite's role, so resending can never hand out a role the caller could not grant. Requires `member:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Invite id (`inv_…`)."}]}},"/invite/{id}":{"delete":{"operationId":"deleteInvite","summary":"Revoke pending invite","tags":["Invites"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible: the emailed link stops working immediately. Requires `member:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Invite id (`inv_…`)."}]}},"/org-domain":{"get":{"operationId":"listOrgDomains","summary":"List org domains","tags":["Org domains"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Org domain id."},"organization_id":{"type":"string","description":"Owning organization (`org_…`)."},"domain":{"type":"string","description":"DNS name, lowercased (e.g. `acme.com`)."},"verified":{"type":"boolean"},"verification_record":{"type":["string","null"],"description":"The exact DNS TXT value to publish. Present until the domain is verified, then null — the token is of no further use."},"default_role_id":{"type":["string","null"],"description":"Role (`role_…`) explicitly configured for domain auto-join, or null to track the default. Org Owner is refused here. Read `effective_default_role_id` to learn what a join actually grants."},"effective_default_role_id":{"type":["string","null"],"description":"Server-computed: the role a verified-domain auto-join really grants. Equals `default_role_id` when that is set and the role still exists; otherwise the **Member** system role, which is what the join falls back to both when nothing is configured and when the configured role has since been deleted. `null` means even that fallback is missing, so auto-join on this domain fails rather than seating anyone."},"sso_connection_id":{"type":["string","null"],"description":"Linked SSO connection (`sso_…`)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `org:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"post":{"operationId":"createOrgDomain","summary":"Claim org domain","tags":["Org domains"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Org domain id."},"organization_id":{"type":"string","description":"Owning organization (`org_…`)."},"domain":{"type":"string","description":"DNS name, lowercased (e.g. `acme.com`)."},"verified":{"type":"boolean"},"verification_record":{"type":["string","null"],"description":"The exact DNS TXT value to publish. Present until the domain is verified, then null — the token is of no further use."},"default_role_id":{"type":["string","null"],"description":"Role (`role_…`) explicitly configured for domain auto-join, or null to track the default. Org Owner is refused here. Read `effective_default_role_id` to learn what a join actually grants."},"effective_default_role_id":{"type":["string","null"],"description":"Server-computed: the role a verified-domain auto-join really grants. Equals `default_role_id` when that is set and the role still exists; otherwise the **Member** system role, which is what the join falls back to both when nothing is configured and when the configured role has since been deleted. `null` means even that fallback is missing, so auto-join on this domain fails rather than seating anyone."},"sso_connection_id":{"type":["string","null"],"description":"Linked SSO connection (`sso_…`)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Claims a DNS name for this organization. The response carries `verification_record` — publish that exact TXT value, then call `POST /org-domain/{id}/verify`. Unverified domains grant nothing. Requires `org:manage`. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. It is gated because a verified domain and an SSO connection are DEFERRED membership grants: they admit a principal with no actor present at the moment they fire, which is `POST /invite` with a longer fuse.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["domain"],"properties":{"domain":{"type":"string","description":"A valid DNS name, e.g. `acme.com`. Lowercased."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/org-domain/{id}":{"patch":{"operationId":"patchOrgDomain","summary":"Update org domain defaults","tags":["Org domains"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Org domain id."},"organization_id":{"type":"string","description":"Owning organization (`org_…`)."},"domain":{"type":"string","description":"DNS name, lowercased (e.g. `acme.com`)."},"verified":{"type":"boolean"},"verification_record":{"type":["string","null"],"description":"The exact DNS TXT value to publish. Present until the domain is verified, then null — the token is of no further use."},"default_role_id":{"type":["string","null"],"description":"Role (`role_…`) explicitly configured for domain auto-join, or null to track the default. Org Owner is refused here. Read `effective_default_role_id` to learn what a join actually grants."},"effective_default_role_id":{"type":["string","null"],"description":"Server-computed: the role a verified-domain auto-join really grants. Equals `default_role_id` when that is set and the role still exists; otherwise the **Member** system role, which is what the join falls back to both when nothing is configured and when the configured role has since been deleted. `null` means even that fallback is missing, so auto-join on this domain fails rather than seating anyone."},"sso_connection_id":{"type":["string","null"],"description":"Linked SSO connection (`sso_…`)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Merge-patch: omit a field to leave it alone, send `null` to clear it. `default_role_id` is the role granted on domain auto-join — Org Owner is refused, since that would auto-elevate anyone with an address on the domain. `sso_connection_id` must name an SSO connection in this organization. Requires `org:manage`. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. It is gated because a verified domain and an SSO connection are DEFERRED membership grants: they admit a principal with no actor present at the moment they fire, which is `POST /invite` with a longer fuse.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"default_role_id":{"type":["string","null"],"description":"Role id (`role_…`), or null to clear."},"sso_connection_id":{"type":["string","null"],"description":"SSO connection id (`sso_…`), or null to unlink."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Org domain id."}]},"delete":{"operationId":"deleteOrgDomain","summary":"Delete org domain","tags":["Org domains"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible: the claim, its verification and any auto-join or SSO routing it provided are gone, and re-claiming requires publishing a fresh TXT record. Existing members are unaffected. Requires `org:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Org domain id."}]}},"/org-domain/{id}/verify":{"post":{"operationId":"verifyOrgDomain","summary":"Verify org domain DNS TXT","tags":["Org domains"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Org domain id."},"organization_id":{"type":"string","description":"Owning organization (`org_…`)."},"domain":{"type":"string","description":"DNS name, lowercased (e.g. `acme.com`)."},"verified":{"type":"boolean"},"verification_record":{"type":["string","null"],"description":"The exact DNS TXT value to publish. Present until the domain is verified, then null — the token is of no further use."},"default_role_id":{"type":["string","null"],"description":"Role (`role_…`) explicitly configured for domain auto-join, or null to track the default. Org Owner is refused here. Read `effective_default_role_id` to learn what a join actually grants."},"effective_default_role_id":{"type":["string","null"],"description":"Server-computed: the role a verified-domain auto-join really grants. Equals `default_role_id` when that is set and the role still exists; otherwise the **Member** system role, which is what the join falls back to both when nothing is configured and when the configured role has since been deleted. `null` means even that fallback is missing, so auto-join on this domain fails rather than seating anyone."},"sso_connection_id":{"type":["string","null"],"description":"Linked SSO connection (`sso_…`)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","properties":{"verified":{"type":"boolean","description":"Outcome of this attempt."},"detail":{"type":["string","null"],"description":"Why verification failed, when it did."}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Runs the DNS TXT challenge now. Idempotent and safe to poll. Returns the domain plus `detail`, which explains a failed lookup (`detail` appears only on this response). Requires `org:manage`. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. Verification is the step that ARMS auto-join, so it is gated alongside the claim and the default-role update.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Org domain id."}]}},"/api-token":{"get":{"operationId":"listApiTokens","summary":"List API tokens","tags":["API tokens"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","description":"An org-scoped API token for these REST endpoints — not an MCP OAuth grant (`GET /oauth/grant`) and not a SCIM token (`GET /scim-token`).","properties":{"id":{"type":"string","description":"API token id (`atok_…`)."},"organization_id":{"type":"string","description":"The single organization this token can address (`org_…`)."},"user_id":{"type":"string","description":"Member (`usr_…`) the token acts as. It carries that member's permissions, live."},"name":{"type":"string"},"last_used_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Metadata only — raw tokens are never returned after creation. Shows your own tokens; with `api_token:view` or `api_token:manage` it shows every token in the organization. Requires one of `api_token:create`, `api_token:view` or `api_token:manage`, and an interactive human session.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the token's name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]},"post":{"operationId":"createApiToken","summary":"Create API token","tags":["API tokens"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"allOf":[{"type":"object","description":"An org-scoped API token for these REST endpoints — not an MCP OAuth grant (`GET /oauth/grant`) and not a SCIM token (`GET /scim-token`).","properties":{"id":{"type":"string","description":"API token id (`atok_…`)."},"organization_id":{"type":"string","description":"The single organization this token can address (`org_…`)."},"user_id":{"type":"string","description":"Member (`usr_…`) the token acts as. It carries that member's permissions, live."},"name":{"type":"string"},"last_used_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","properties":{"token":{"type":"string","description":"Raw token — shown once."}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Mints an org-scoped bearer token of the form `elch_{org_id}_{64 hex}`. The raw `token` appears in THIS response only and cannot be retrieved again — store it now; if it is lost, revoke it and mint another. The token acts as you, with your permissions resolved live, in this organization only. Requires `api_token:create` and an interactive human session (a token cannot mint another token), plus a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`) — minting a credential is what a stolen session would otherwise do to walk around every other step-up gate. Without one the call answers `428 step_up_required`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/api-token/{id}":{"patch":{"operationId":"patchApiToken","summary":"Rename API token","tags":["API tokens"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","description":"An org-scoped API token for these REST endpoints — not an MCP OAuth grant (`GET /oauth/grant`) and not a SCIM token (`GET /scim-token`).","properties":{"id":{"type":"string","description":"API token id (`atok_…`)."},"organization_id":{"type":"string","description":"The single organization this token can address (`org_…`)."},"user_id":{"type":"string","description":"Member (`usr_…`) the token acts as. It carries that member's permissions, live."},"name":{"type":"string"},"last_used_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Merge-patch of the token’s metadata; `name` is the only mutable field, and omitting it is a no-op that returns the record unchanged. The credential itself is untouched — the raw token, its owner, its permissions and `last_used_at` do not change, so renaming never takes a token out of service (use DELETE for that). Requires `api_token:create` or `api_token:manage` and an interactive human session, then enforces owner OR `api_token:manage`. No step-up is required, unlike POST and DELETE: a rename takes nothing away and mints nothing.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120,"description":"New display name."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"API token id (`atok_…`) — not the token itself."}]},"delete":{"operationId":"deleteApiToken","summary":"Revoke API token","tags":["API tokens"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible and immediate: every integration using this token starts failing on its next request. Requires `api_token:create` or `api_token:manage`, an interactive human session, and a fresh step-up authentication (`X-Step-Up-Token`, obtained from `/auth/step-up`).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"API token id (`atok_…`) — not the token itself."}]}},"/audit-log":{"get":{"operationId":"listAuditLog","summary":"List audit log events","tags":["Audit log"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Audit entry id (`aud_…`)."},"actor_user_id":{"type":["string","null"],"description":"Null for system-generated entries (`usr_…` otherwise)."},"action":{"type":"string","description":"Dotted action name, e.g. `connection.deleted`."},"resource_type":{"type":["string","null"]},"resource_id":{"type":["string","null"],"description":"Prefixed id of the affected resource, when there is one."},"metadata":{"type":"object","additionalProperties":true,"description":"Action-specific detail; shape varies by action."},"created_at":{"type":"string","format":"date-time"},"category":{"type":"string"},"action_kind":{"type":"string"},"actor_kind":{"type":"string","enum":["user","system","staff","scim","api_token","ai_assistant"]},"actor":{"type":"object","description":"The person behind this row, named server-side in one batch per page. Read this rather than resolving `actor_user_id` against the member directory: that directory is paginated, so a client resolving ids itself only names the actors on the page it happened to fetch.","properties":{"user_id":{"type":["string","null"]},"name":{"type":["string","null"],"description":"Null (with `email`) when the id is no longer resolvable in this organization — a departed member, which clients render as \"Former member\"."},"email":{"type":["string","null"]},"kind":{"type":"string","description":"Mirrors `actor_kind`, so a non-human actor stays distinguishable from an unnamed one."}}}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Append-only trail of what happened in this organization, newest first. Requires `audit:view`. Filters combine with AND; `q` additionally matches the actor by name or email. The cursor is opaque — do not parse or construct one. Events are eventually consistent: one may take a moment to appear after the action that produced it.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Matches the action, the resource, or the actor by name/email. `system` matches system-generated rows. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"category","in":"query","required":false,"description":"Bucket derived from the action name. Any other value is a 400.","schema":{"type":"string","enum":["auth","assistant","mcp","toolbox","connection","access","org","other"]}},{"name":"action","in":"query","required":false,"description":"Verb class of the action, not the full action name.","schema":{"type":"string","enum":["created","updated","deleted","other"]}},{"name":"actor","in":"query","required":false,"description":"A user id (`usr_…`), or the literal `system` for rows with no human actor. Max 128 characters.","schema":{"type":"string","maxLength":128}},{"name":"since","in":"query","required":false,"description":"Inclusive lower bound on `created_at`. ISO-8601; anything unparseable is a 400.","schema":{"type":"string","format":"date-time"}}]}},"/connector":{"get":{"operationId":"listConnectors","summary":"List connectors","tags":["Connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string","description":"Stable connector key — this is the identifier everywhere, not an id."},"label":{"type":"string"},"category":{"type":"string"},"logo":{"type":["string","null"]},"description":{"type":["string","null"]},"owner":{"type":"string","description":"Catalog owner. Org-owned values mark this org's custom connectors."},"custom":{"type":"boolean","description":"True for a connector this organization authored or forked."},"byoa":{"type":"boolean","description":"True when the connector needs a customer OAuth app (BYOA) — a placeholder client id, a real id with a placeholder secret, or a real id with no platform OAuth secret stored for it. The first two are derived from the config on the catalog summary, so they are on every row; the third comes from one cached platform-credential read."},"owner_user_id":{"type":["string","null"],"description":"Always `null`. Unlike a connection or toolbox, a custom connector has no durable per-user owner — only `resource_acl` grants (the creator receives an ordinary, revocable `edit` grant at creation time, the same as anyone shared with later; see customConnectorRouter.ts's module doc). Kept for shape parity with the other two resources' summaries."},"can_manage":{"type":"boolean","description":"Present only on org-owned/custom connector rows (omitted for platform catalog rows, which have no ACL concept). Whether the caller may edit this connector's config — a real `edit` grant of their own. `connector:manage` alone no longer implies this (removed 2026-09-04): org owners/admins do not manage-by-default a connector nobody explicitly shared with them."},"restricted":{"type":"boolean","description":"True when the caller's restrictions block this whole connector. A blocked connector is LISTED and flagged, never omitted — a picker that only got \"no match\" could not tell \"your admin blocked it\" from \"that provider does not exist\". It still cannot be connected: `POST /connection` and `GET /connector/{slug}/tools` refuse it."},"restricted_by":{"type":["string","null"],"enum":["role","user",null],"description":"Which precedence layer the winning rule came from, or null when nothing blocks this connector. Adds *which* to `restricted`'s *whether* and nothing else — no rule id, author, reason or coverage. Same field and same meaning as on `GET /connector/{slug}` and on a connection row."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this connector — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the connector reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this connector through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The platform catalog — visible to every role, `connector:view` included — plus this organization's own custom connectors, which (unlike connections and toolboxes) are already browseable by every member regardless of any grant: a custom connector's existence, name and category are catalog metadata, not a private resource (`connector:view` is the universal catalog-browse permission; see customConnectorRouter.ts's module doc). There is no `?visibility=org` oversight mode here (removed 2026-09-04): `connector:manage` no longer surfaces per-row attribution for a connector the caller was never explicitly granted access to — `can_manage` on an org-owned row reflects only a real `edit` grant of their own, present on every row regardless of any query parameter. Platform catalog rows omit `can_manage` (no ACL concept). Connectors the caller's restrictions block are RETURNED, flagged `restricted`/`restricted_by`, not filtered out — so a restricted caller's page is the same length as anyone else's. `prev_cursor` is always null here.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"search","in":"query","description":"Substring match on the connector name/label.","schema":{"type":"string"}},{"name":"category","in":"query","description":"Exact category match.","schema":{"type":"string"}},{"name":"custom","in":"query","description":"Send `true` to narrow to this organization's own connectors.","schema":{"type":"boolean"}},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"post":{"operationId":"createCustomConnector","summary":"Create custom connector","tags":["Custom connectors"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Authors a new org-owned connector from a catalog config. Requires `connector:create` and the `custom_connectors` plan feature. The config is schema-validated: a malformed one answers 422 with the exact issues. The slug is permanent and must be unique within the catalog.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["slug","label","config"],"properties":{"slug":{"type":"string","minLength":2,"maxLength":64,"pattern":"^[a-z0-9-]+$","description":"Lowercase letters, digits and hyphens. `new` is reserved."},"label":{"type":"string","minLength":1,"maxLength":120},"category":{"type":["string","null"],"minLength":1,"maxLength":80},"config":{"type":"object","additionalProperties":true,"description":"Connector definition (resources, methods, auth)."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/connector/categories":{"get":{"operationId":"listConnectorCategories","summary":"List distinct connector categories","tags":["Connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result"],"properties":{"result":{"type":"array","items":{"type":"string"},"description":"Sorted, de-duplicated category values."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Every category value used across the catalog this organization can see — the platform catalog plus its own custom connectors — from a full scan, not just whatever page `GET /connector` happens to have returned so far. Use this to populate a category filter; use `GET /connector?category=` to apply one. Briefly cached server-side, so a category added or removed by a custom-connector edit can take a short while to show up here. Requires `connector:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/connector/{slug}":{"get":{"operationId":"getConnector","summary":"Get connector detail","tags":["Connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","additionalProperties":true},{"type":"object","properties":{"restricted":{"type":"boolean"}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Catalog detail plus `restricted`, which reports whether the caller's restrictions block this connector — a restricted connector still resolves here but cannot be connected or executed. For an org-owned connector the response also carries its config, `can_manage`, `can_share` (mirrors `POST /:slug/share`'s own gate: edit access AND `connector:share`, never a `connector:manage` fallback), the bounded `access_summary` (same shape as connection/toolbox's — counts plus a few resolved names; page `GET /:slug/share` for the full grant list) and `shares`, present only when the caller has a real `edit` grant of their own — never for a bare `connector:manage` holder with no grant (removed 2026-09-04: org owners/admins do not see access details nobody explicitly shared with them, and there is no oversight audit trail for connectors any more). A custom connector the caller cannot see is `404`, never `403`. Platform catalog connectors need only `connector:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug (e.g. `hubspot`) — not an id."}]},"patch":{"operationId":"patchCustomConnector","summary":"Update custom connector","tags":["Custom connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"**`config` is REPLACED wholesale when present** — send the complete definition, since anything omitted is gone. `label` and `category` are ordinary merge-patch fields. Org-owned connectors only, and the caller needs edit access (ownership or an `edit` share). Existing connections keep working, but tools removed from the config stop resolving in every toolbox that names them.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":120},"category":{"type":["string","null"],"minLength":1,"maxLength":80},"config":{"type":"object","additionalProperties":true,"description":"Replaces the stored config entirely."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned connector."}]},"delete":{"operationId":"deleteCustomConnector","summary":"Delete custom connector","tags":["Custom connectors"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible. Refused with 409 while any connection still uses this connector — delete or transfer those first — so it cannot strand vaulted accounts or toolbox tools. Org-owned connectors only, edit access required.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned connector."}]}},"/connector/{slug}/tools":{"get":{"operationId":"listConnectorTools","summary":"List connector tools (catalog capability)","tags":["Connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Exact tool name — what a toolbox entry's `tool_name` must contain."},"description":{"type":"string"},"resource":{"type":"string"},"method":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true,"description":"JSON Schema for the tool arguments."},"restricted":{"type":"boolean","description":"True when the caller's restrictions block THIS tool. Blocked tools are returned flagged, never omitted — a shorter list is no signal, because nobody knows the length it should have been. Presence is not permission: execution still refuses it. The connector-level 403 is a separate question and is unchanged."},"restricted_by":{"type":["string","null"],"enum":["role","user",null],"description":"Which precedence layer's rule blocks this tool, null when none does. Same field, union and meaning as on connector and connection rows: adds *which* to `restricted`'s *whether* and nothing else — no rule id, author, reason or coverage. `restricted === (restricted_by !== null)` always."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Every tool this connector defines, each flagged `restricted`/`restricted_by` for the caller — the catalog view, with no account behind it. Use it to pick `tool_name` values for toolbox entries. For what one authenticated account can actually run, use `GET /connection/{id}/tools`. Blocked tools are flagged, not omitted; the model-facing `connector.list_tools` operation still omits them, because a tool a model may not call is a wasted turn while a person picking one needs to see why it is unavailable. A restricted connector answers 403. Requires `connector:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug."}]}},"/connector/{slug}/fork":{"post":{"operationId":"forkConnector","summary":"Fork connector into an org-owned copy","tags":["Custom connectors"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Copies a catalog connector into this organization under a new slug, tracking the original as upstream so later changes can be pulled. Forking a connector the caller's restrictions block is refused — otherwise a fork would launder it under a slug no restriction names. Requires `connector:create`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"new_slug":{"type":"string","minLength":2,"maxLength":64,"pattern":"^[a-z0-9-]+$","description":"Defaults to a derived slug."},"label":{"type":"string","minLength":1,"maxLength":120}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug to fork."}]}},"/connector/{slug}/pull":{"get":{"operationId":"previewConnectorPull","summary":"Preview upstream changes for a forked connector","tags":["Custom connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Read-only diff against the upstream connector, returning individually selectable change ids for `POST /connector/{slug}/pull`. A fork with no upstream answers 400 — link one first with `POST /connector/{slug}/link-upstream`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned fork."}]},"post":{"operationId":"applyConnectorPull","summary":"Apply selected upstream changes","tags":["Custom connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Applies exactly the changes whose ids you list, from the preview — nothing else is touched. Re-run the preview first: ids are computed from the current diff and go stale.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["change_ids"],"properties":{"change_ids":{"type":"array","maxItems":500,"items":{"type":"string","maxLength":200},"description":"Ids from the preview."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned fork."}]}},"/connector/{slug}/link-upstream":{"post":{"operationId":"linkConnectorUpstream","summary":"Attach upstream lineage","tags":["Custom connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Points an older fork at an upstream connector so it can use the pull workflow. New forks are linked automatically; re-linking an already-linked connector answers 409.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["upstream_slug"],"properties":{"upstream_slug":{"type":"string","description":"Catalog connector to track."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned fork."}]},"delete":{"operationId":"unlinkConnectorUpstream","summary":"Detach upstream lineage","tags":["Custom connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Removes the connector's declared upstream link. Needs `connector:manage`, not merely edit on the connector: a restriction on an upstream also binds everything declared downstream of it, so detaching that link would otherwise let a fork's own author shed a block while keeping the connector's live connections. Use it to repair a lineage row pointed at the wrong upstream; `POST` the correct one afterwards.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned connector."}]}},"/connector/{slug}/documentation":{"put":{"operationId":"putConnectorDocumentation","summary":"Bulk upsert connector documentation","tags":["Custom connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"result":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Documentation is what turns a connector method into a tool: an item with an empty `description` UN-documents that method, and an undocumented method is not offered as a tool. Items must reference methods that exist in the config. An item with neither `resource` nor `method` sets the connector-level description. Sent items are upserted; methods you do not mention are left as they are.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","maxItems":500,"items":{"type":"object","required":["description"],"properties":{"resource":{"type":["string","null"],"minLength":1,"maxLength":200},"method":{"type":["string","null"],"minLength":1,"maxLength":200},"description":{"type":"string","maxLength":8000,"description":"Empty string un-documents the method."},"query_schema":{"type":["object","null"],"additionalProperties":true},"body_schema":{"type":["object","null"],"additionalProperties":true}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned connector."}]}},"/connector/{slug}/oauth-app":{"get":{"operationId":"getConnectorOAuthApp","summary":"Read this org’s OAuth application for a connector","tags":["Connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"oauth_app_mode":{"type":"string","enum":["platform_default","byoa"],"description":"Which application connects on this connector use."},"client_id":{"type":["string","null"]},"has_client_secret":{"type":"boolean","description":"Whether a secret is stored. The secret itself is never returned."},"scope":{"type":["array","null"],"items":{"type":"string"},"description":"Scope overrides this org stored, or null when it kept the catalog scopes."},"optional_scope":{"type":["array","null"],"items":{"type":"string"}},"user_scope":{"type":["string","null"],"description":"Space-separated authorize-URL scopes (`params.user_scope`), or null."},"updated_at":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Whether connects on this connector use Elaichi’s platform application or one this organization supplied, and the client id if so. The client secret is never returned — `has_client_secret` reports only whether one is stored. Answers `404` for a connector this organization owns (edit its credentials with `PATCH /connector/{slug}` instead) and for one that does not use a customer OAuth application at all, so a read cannot be used to probe which is which. Requires `connector:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug."}]},"put":{"operationId":"putConnectorOAuthApp","summary":"Set or rotate this org’s OAuth application","tags":["Connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"oauth_app_mode":{"type":"string","enum":["platform_default","byoa"],"description":"Which application connects on this connector use."},"client_id":{"type":["string","null"]},"has_client_secret":{"type":"boolean","description":"Whether a secret is stored. The secret itself is never returned."},"scope":{"type":["array","null"],"items":{"type":"string"},"description":"Scope overrides this org stored, or null when it kept the catalog scopes."},"optional_scope":{"type":["array","null"],"items":{"type":"string"}},"user_scope":{"type":["string","null"],"description":"Space-separated authorize-URL scopes (`params.user_scope`), or null."},"updated_at":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Replaces the stored application wholesale, with one exception the setup form depends on: omitting `client_secret` keeps the secret already stored, so correcting a typo in the client id cannot destroy a working secret. Send both to rotate. Scope overrides (`scope`, `optional_scope`, `user_scope`) are optional; omit them to keep the connector’s catalog scopes, or send arrays to replace them. Endpoint fields (token host, other authorize params) are not accepted here by design. Returns the same shape as the read, so the secret does not come back. Requires `connector:manage`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["client_id"],"properties":{"client_id":{"type":"string","maxLength":500},"client_secret":{"type":"string","maxLength":2000,"description":"Omit to keep the stored secret. Never returned by any endpoint."},"scope":{"type":"array","items":{"type":"string","maxLength":500},"maxItems":200,"description":"Omit to keep the catalog scopes; an array overrides them wholesale."},"optional_scope":{"type":"array","items":{"type":"string","maxLength":500},"maxItems":200},"user_scope":{"type":"string","maxLength":2000,"description":"Space-separated authorize-URL scopes (`params.user_scope`)."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug."}]},"delete":{"operationId":"clearConnectorOAuthApp","summary":"Drop this org’s OAuth application","tags":["Connectors"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Removes the override so later connects fall back to Elaichi’s platform application. Existing connections made through the removed application are not revoked. Requires `connector:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug."}]}},"/connector/{slug}/share":{"get":{"operationId":"listConnectorShares","summary":"List connector ACL shares","tags":["Custom connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"$ref":"#/components/schemas/AclEntry"}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Cursor-paginated, the same shape as `GET /connection/{id}/share` and `GET /toolbox/{id}/share` — a connector may be shared with every member of the organization, so nothing embeds the grant list unbounded. Filter with `grantee_type`, search member names/emails and team names with `q`. Org-owned connectors only. Requires the same access as seeing the ACL on the connector itself: `edit` access or `connector:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned connector."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Match member name or email, or team name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"grantee_type","in":"query","required":false,"description":"Restrict to one kind of grantee.","schema":{"type":"string","enum":["user","team","org"]}}]},"post":{"operationId":"shareConnector","summary":"Share custom connector","tags":["Custom connectors"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AclEntry"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Grants a user, team, or the whole org access to an org-owned connector at a level — the same `view` < `use` < `edit` ladder every shareable resource uses (docs/access-model.md §3): `use` here means creating connections from this connector, `edit` additionally means changing its config and its own grants. Requires `edit` access or ownership, and `connector:share`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareRequest"}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned connector."}]}},"/connector/{slug}/share/{aclId}":{"delete":{"operationId":"revokeConnectorShare","summary":"Revoke connector share","tags":["Custom connectors"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The grantee loses access immediately. Connections already created on the connector keep working.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug."},{"name":"aclId","in":"path","required":true,"schema":{"type":"string"},"description":"ACL entry id from the share list."}]}},"/connection":{"get":{"operationId":"listConnections","summary":"List connections","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (`conn_…`)."},"connector_slug":{"type":"string","description":"Which connector this account is on."},"name":{"type":"string"},"owner_user_id":{"type":"string","description":"Owning member (`usr_…`); calls run as this identity. Owner is implicit `edit`, plus delete and transfer — the two things no share confers. Move it with `POST /connection/{id}/transfer`. This is who is ACCOUNTABLE for the connection now, which after a transfer is not who authenticated it — see `connected_by_user_id`."},"connected_by_user_id":{"type":["string","null"],"description":"Who AUTHENTICATED this account (`usr_…`) — the member whose third-party credential the vault actually holds. Written once at creation and never rewritten. Distinct from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and deliberately does NOT touch the vault account, so after a transfer these are two different people and only this one may be rendered as \"Connected by\". **Null means NOT RECORDED, never \"nobody\"** — every connection created before this field existed is null, because nothing server-side could tell a never-transferred row (where `owner_user_id` would have been the honest answer) from a transferred one, and a fabricated attribution is worse than an absent one. Render nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id`, resolved server-side one batch per page (name, else email). Present on every branch that returns a connection, commands included. A null HERE beside a NON-null `connected_by_user_id` is a fact about the person, not a failed lookup: they have left the organization, and clients render \"Former member\" — the same word the `GET /audit-log` actor summary uses. Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"saffron_account_id":{"type":["string","null"],"description":"Vault account holding the credential. Null until the connect flow completes."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected"],"description":"`pending` = created but not yet authenticated (open `connect_url`); `active` = usable; `needs_reauth` = the credential expired or was revoked upstream, call reconnect; `disconnected` = no longer usable. Refreshed lazily against the vault when connections are read."},"last_error":{"type":["string","null"]},"access":{"type":"string","description":"The caller's relationship to this connection: `owner`, or a granted `view`/`use`/`edit` level. There is no oversight fallback for connections — this is never `\"oversight\"`; a connection the caller neither owns nor holds a grant on is not visible to them at all, whatever organization permission they hold."},"owner":{"type":["object","null"],"description":"Owner summary. Currently never populated on a connection response — no list route resolves it today — but kept in the schema in case a future route does; read `owner_user_id` plus a member lookup instead.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"shares":{"type":"array","description":"Present only when the caller may see the ACL: owner or `edit` access. A `view`/`use` recipient is never told who else the connection is shared with. Absent (not empty) when the caller may not see it.","items":{"$ref":"#/components/schemas/AclEntry"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"can_share":{"type":"boolean","description":"Whether the caller may share this connection — owner, `edit` access, or a team admin on a team-granted one. No `connection:manage` fallback. Computed server-side (CLAUDE.md's API-first rule); the console reads this rather than re-deriving it from `owner_user_id`/permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings — owner, or `edit` access. No `connection:manage` fallback. Computed server-side; same `can_manage` name used by toolbox and connector for the identical capability."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection — owner, full stop. No `connection:manage` fallback. Deliberately narrower than `can_manage`: an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, deliberately not an alias for `can_manage`: an `edit` grantee whose role omits `connection:share` (Auditor, Guest, any custom role) can reconfigure or reconnect the connection but was never meant to grant or revoke someone else's access to it, so `can_manage: true` on that row must not imply this control too. Mirrors `DELETE /connection/{id}/share/{aclId}`'s own gate exactly. Present on every list and detail row."},"access_summary":{"type":"object","properties":{"org_level":{"type":["string","null"],"enum":["view","use",null]},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}},"description":"Present only when `can_share` is true. Bounded by design — page `GET /connection/{id}/share` for the grants themselves."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this connection — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the connection reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this connection through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Exactly docs/access-model.md §4, always — connections the caller owns, plus every connection shared with them directly, through a team they belong to, or org-wide. No organization permission widens this: a private connection (no grants) never appears here for anyone but its owner, whatever permission an org owner or admin holds. There is no org-wide oversight listing for connections. Statuses are reconciled against the vault as they are read, so this is also how a `needs_reauth` connection surfaces. Rows the caller may share (owner or `edit` access) also carry `can_share: true` and a bounded `access_summary` — see the schema; page `GET /connection/{id}/share` for the grants themselves.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Substring match (`LIKE \"%value%\"`, case-insensitive) on the connection name, the connected account label where present, the connector slug (\"slack\", \"google-drive\"), and the connector's catalog label (\"Google Drive\") — matching the search box's own \"Search by name or connector…\" placeholder against both spellings a connector can go by. `connector_slug` below is a different question: an exact-equality filter to only these connectors, not a search. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"status","in":"query","required":false,"description":"Filter by lifecycle status. Any other value is a 400.","schema":{"type":"string","enum":["pending","active","needs_reauth","disconnected"]}},{"name":"connector_slug","in":"query","required":false,"description":"Filter to one or more connectors. Repeat the parameter or send one comma-separated value — both are the same filter. At most 25 values per request; more is a 400.","explode":true,"schema":{"type":"array","maxItems":25,"items":{"type":"string"}}},{"name":"owner_user_id","in":"query","required":false,"description":"Filter to connections owned by one user. This narrows what the caller can already see; it is not a second access path, so it never surfaces another member’s private connections. An id that matches nobody returns an empty page rather than a 404.","schema":{"type":"string"}}]},"post":{"operationId":"createConnection","summary":"Create connection","tags":["Connections"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","required":["connection","connect_url"],"properties":{"connection":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (`conn_…`)."},"connector_slug":{"type":"string","description":"Which connector this account is on."},"name":{"type":"string"},"owner_user_id":{"type":"string","description":"Owning member (`usr_…`); calls run as this identity. Owner is implicit `edit`, plus delete and transfer — the two things no share confers. Move it with `POST /connection/{id}/transfer`. This is who is ACCOUNTABLE for the connection now, which after a transfer is not who authenticated it — see `connected_by_user_id`."},"connected_by_user_id":{"type":["string","null"],"description":"Who AUTHENTICATED this account (`usr_…`) — the member whose third-party credential the vault actually holds. Written once at creation and never rewritten. Distinct from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and deliberately does NOT touch the vault account, so after a transfer these are two different people and only this one may be rendered as \"Connected by\". **Null means NOT RECORDED, never \"nobody\"** — every connection created before this field existed is null, because nothing server-side could tell a never-transferred row (where `owner_user_id` would have been the honest answer) from a transferred one, and a fabricated attribution is worse than an absent one. Render nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id`, resolved server-side one batch per page (name, else email). Present on every branch that returns a connection, commands included. A null HERE beside a NON-null `connected_by_user_id` is a fact about the person, not a failed lookup: they have left the organization, and clients render \"Former member\" — the same word the `GET /audit-log` actor summary uses. Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"saffron_account_id":{"type":["string","null"],"description":"Vault account holding the credential. Null until the connect flow completes."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected"],"description":"`pending` = created but not yet authenticated (open `connect_url`); `active` = usable; `needs_reauth` = the credential expired or was revoked upstream, call reconnect; `disconnected` = no longer usable. Refreshed lazily against the vault when connections are read."},"last_error":{"type":["string","null"]},"access":{"type":"string","description":"The caller's relationship to this connection: `owner`, or a granted `view`/`use`/`edit` level. There is no oversight fallback for connections — this is never `\"oversight\"`; a connection the caller neither owns nor holds a grant on is not visible to them at all, whatever organization permission they hold."},"owner":{"type":["object","null"],"description":"Owner summary. Currently never populated on a connection response — no list route resolves it today — but kept in the schema in case a future route does; read `owner_user_id` plus a member lookup instead.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"shares":{"type":"array","description":"Present only when the caller may see the ACL: owner or `edit` access. A `view`/`use` recipient is never told who else the connection is shared with. Absent (not empty) when the caller may not see it.","items":{"$ref":"#/components/schemas/AclEntry"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"can_share":{"type":"boolean","description":"Whether the caller may share this connection — owner, `edit` access, or a team admin on a team-granted one. No `connection:manage` fallback. Computed server-side (CLAUDE.md's API-first rule); the console reads this rather than re-deriving it from `owner_user_id`/permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings — owner, or `edit` access. No `connection:manage` fallback. Computed server-side; same `can_manage` name used by toolbox and connector for the identical capability."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection — owner, full stop. No `connection:manage` fallback. Deliberately narrower than `can_manage`: an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, deliberately not an alias for `can_manage`: an `edit` grantee whose role omits `connection:share` (Auditor, Guest, any custom role) can reconfigure or reconnect the connection but was never meant to grant or revoke someone else's access to it, so `can_manage: true` on that row must not imply this control too. Mirrors `DELETE /connection/{id}/share/{aclId}`'s own gate exactly. Present on every list and detail row."},"access_summary":{"type":"object","properties":{"org_level":{"type":["string","null"],"enum":["view","use",null]},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}},"description":"Present only when `can_share` is true. Bounded by design — page `GET /connection/{id}/share` for the grants themselves."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this connection — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the connection reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this connection through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},"connect_url":{"type":"string","format":"uri","description":"Where the USER authenticates with the provider. Hand it to them as a link — Elaichi never accepts a password or API key for a third-party system through this API. Single-use and short-lived; mint another with reconnect. On completion the provider returns to `GET /connection/{id}/callback` and the status becomes `active`."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Creates a `pending` connection and returns a `connect_url` for the user to open — authentication happens with the provider, never through this API, so no credential is ever sent here. Poll `GET /connection/{id}` until the status is `active`. The caller becomes `owner_user_id`. Optional `shares` grants access at creation time, in place of the removed `scope`/`team_id`: absent or `[]` makes a private connection, reachable only by its owner, and needs only `connection:create`. Any non-empty `shares` additionally needs `connection:share`, because a shared connection hands its account to other members the instant it is created.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["connector_slug"],"properties":{"connector_slug":{"type":"string","description":"From `GET /connector` — a slug, not an id."},"redirect_uri":{"type":"string","maxLength":2000,"description":"App-relative path to return the user to after the connect flow. Absolute URLs are ignored."},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Defaults to a connector-derived name."},"shares":{"type":"array","maxItems":200,"description":"Grants to create alongside the connection. Omit, or send `[]`, for a private connection.","items":{"$ref":"#/components/schemas/ShareRequest"}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/connection/{id}":{"get":{"operationId":"getConnection","summary":"Get connection","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (`conn_…`)."},"connector_slug":{"type":"string","description":"Which connector this account is on."},"name":{"type":"string"},"owner_user_id":{"type":"string","description":"Owning member (`usr_…`); calls run as this identity. Owner is implicit `edit`, plus delete and transfer — the two things no share confers. Move it with `POST /connection/{id}/transfer`. This is who is ACCOUNTABLE for the connection now, which after a transfer is not who authenticated it — see `connected_by_user_id`."},"connected_by_user_id":{"type":["string","null"],"description":"Who AUTHENTICATED this account (`usr_…`) — the member whose third-party credential the vault actually holds. Written once at creation and never rewritten. Distinct from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and deliberately does NOT touch the vault account, so after a transfer these are two different people and only this one may be rendered as \"Connected by\". **Null means NOT RECORDED, never \"nobody\"** — every connection created before this field existed is null, because nothing server-side could tell a never-transferred row (where `owner_user_id` would have been the honest answer) from a transferred one, and a fabricated attribution is worse than an absent one. Render nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id`, resolved server-side one batch per page (name, else email). Present on every branch that returns a connection, commands included. A null HERE beside a NON-null `connected_by_user_id` is a fact about the person, not a failed lookup: they have left the organization, and clients render \"Former member\" — the same word the `GET /audit-log` actor summary uses. Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"saffron_account_id":{"type":["string","null"],"description":"Vault account holding the credential. Null until the connect flow completes."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected"],"description":"`pending` = created but not yet authenticated (open `connect_url`); `active` = usable; `needs_reauth` = the credential expired or was revoked upstream, call reconnect; `disconnected` = no longer usable. Refreshed lazily against the vault when connections are read."},"last_error":{"type":["string","null"]},"access":{"type":"string","description":"The caller's relationship to this connection: `owner`, or a granted `view`/`use`/`edit` level. There is no oversight fallback for connections — this is never `\"oversight\"`; a connection the caller neither owns nor holds a grant on is not visible to them at all, whatever organization permission they hold."},"owner":{"type":["object","null"],"description":"Owner summary. Currently never populated on a connection response — no list route resolves it today — but kept in the schema in case a future route does; read `owner_user_id` plus a member lookup instead.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"shares":{"type":"array","description":"Present only when the caller may see the ACL: owner or `edit` access. A `view`/`use` recipient is never told who else the connection is shared with. Absent (not empty) when the caller may not see it.","items":{"$ref":"#/components/schemas/AclEntry"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"can_share":{"type":"boolean","description":"Whether the caller may share this connection — owner, `edit` access, or a team admin on a team-granted one. No `connection:manage` fallback. Computed server-side (CLAUDE.md's API-first rule); the console reads this rather than re-deriving it from `owner_user_id`/permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings — owner, or `edit` access. No `connection:manage` fallback. Computed server-side; same `can_manage` name used by toolbox and connector for the identical capability."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection — owner, full stop. No `connection:manage` fallback. Deliberately narrower than `can_manage`: an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, deliberately not an alias for `can_manage`: an `edit` grantee whose role omits `connection:share` (Auditor, Guest, any custom role) can reconfigure or reconnect the connection but was never meant to grant or revoke someone else's access to it, so `can_manage: true` on that row must not imply this control too. Mirrors `DELETE /connection/{id}/share/{aclId}`'s own gate exactly. Present on every list and detail row."},"access_summary":{"type":"object","properties":{"org_level":{"type":["string","null"],"enum":["view","use",null]},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}},"description":"Present only when `can_share` is true. Bounded by design — page `GET /connection/{id}/share` for the grants themselves."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this connection — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the connection reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this connection through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Status is reconciled against the vault on read. Connections the caller cannot see answer 404, never 403 — including another member's private connection, which no organization permission reaches. Resolves docs/access-model.md §4 ONLY, with no opt-in of any kind: the caller owns the connection, or holds a grant on it (directly, through a team, or org-wide). No grant, no fallback, `404` — even for a holder of `connection:view`/`connection:manage`. There is no oversight read for connections; a `visibility` query parameter is not accepted. `can_share`, `can_manage` and `can_transfer` report whether the caller may share, edit or transfer/delete this connection, respectively — see the schema for each; none of the three has a `connection:manage` fallback any more (removed 2026-09-04) — ownership or a real grant are the only ways to reach them. `access_summary`, a bounded \"who has access\" rollup, is present only when `can_share` is true — page `GET /connection/{id}/share` for the grants themselves.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]},"patch":{"operationId":"patchConnection","summary":"Rename connection","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (`conn_…`)."},"connector_slug":{"type":"string","description":"Which connector this account is on."},"name":{"type":"string"},"owner_user_id":{"type":"string","description":"Owning member (`usr_…`); calls run as this identity. Owner is implicit `edit`, plus delete and transfer — the two things no share confers. Move it with `POST /connection/{id}/transfer`. This is who is ACCOUNTABLE for the connection now, which after a transfer is not who authenticated it — see `connected_by_user_id`."},"connected_by_user_id":{"type":["string","null"],"description":"Who AUTHENTICATED this account (`usr_…`) — the member whose third-party credential the vault actually holds. Written once at creation and never rewritten. Distinct from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and deliberately does NOT touch the vault account, so after a transfer these are two different people and only this one may be rendered as \"Connected by\". **Null means NOT RECORDED, never \"nobody\"** — every connection created before this field existed is null, because nothing server-side could tell a never-transferred row (where `owner_user_id` would have been the honest answer) from a transferred one, and a fabricated attribution is worse than an absent one. Render nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id`, resolved server-side one batch per page (name, else email). Present on every branch that returns a connection, commands included. A null HERE beside a NON-null `connected_by_user_id` is a fact about the person, not a failed lookup: they have left the organization, and clients render \"Former member\" — the same word the `GET /audit-log` actor summary uses. Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"saffron_account_id":{"type":["string","null"],"description":"Vault account holding the credential. Null until the connect flow completes."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected"],"description":"`pending` = created but not yet authenticated (open `connect_url`); `active` = usable; `needs_reauth` = the credential expired or was revoked upstream, call reconnect; `disconnected` = no longer usable. Refreshed lazily against the vault when connections are read."},"last_error":{"type":["string","null"]},"access":{"type":"string","description":"The caller's relationship to this connection: `owner`, or a granted `view`/`use`/`edit` level. There is no oversight fallback for connections — this is never `\"oversight\"`; a connection the caller neither owns nor holds a grant on is not visible to them at all, whatever organization permission they hold."},"owner":{"type":["object","null"],"description":"Owner summary. Currently never populated on a connection response — no list route resolves it today — but kept in the schema in case a future route does; read `owner_user_id` plus a member lookup instead.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"shares":{"type":"array","description":"Present only when the caller may see the ACL: owner or `edit` access. A `view`/`use` recipient is never told who else the connection is shared with. Absent (not empty) when the caller may not see it.","items":{"$ref":"#/components/schemas/AclEntry"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"can_share":{"type":"boolean","description":"Whether the caller may share this connection — owner, `edit` access, or a team admin on a team-granted one. No `connection:manage` fallback. Computed server-side (CLAUDE.md's API-first rule); the console reads this rather than re-deriving it from `owner_user_id`/permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings — owner, or `edit` access. No `connection:manage` fallback. Computed server-side; same `can_manage` name used by toolbox and connector for the identical capability."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection — owner, full stop. No `connection:manage` fallback. Deliberately narrower than `can_manage`: an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, deliberately not an alias for `can_manage`: an `edit` grantee whose role omits `connection:share` (Auditor, Guest, any custom role) can reconfigure or reconnect the connection but was never meant to grant or revoke someone else's access to it, so `can_manage: true` on that row must not imply this control too. Mirrors `DELETE /connection/{id}/share/{aclId}`'s own gate exactly. Present on every list and detail row."},"access_summary":{"type":"object","properties":{"org_level":{"type":["string","null"],"enum":["view","use",null]},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}},"description":"Present only when `can_share` is true. Bounded by design — page `GET /connection/{id}/share` for the grants themselves."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this connection — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the connection reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this connection through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Only the display name can be changed here. Ownership moves through `POST /connection/{id}/transfer`; who else can reach it moves through `POST /connection/{id}/share` and `DELETE /connection/{id}/share/{aclId}`; credentials are re-established through `POST /connection/{id}/reconnect`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]},"delete":{"operationId":"deleteConnection","summary":"Delete connection","tags":["Connections"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"IRREVERSIBLE AND DESTRUCTIVE. The vaulted credential is deleted first: the only way back is for the owner to authenticate with the provider again through a fresh connect flow. It also CASCADES — toolbox entries pinned to it stop resolving, and every grant on it is discarded with it. To move a connection to someone else instead, use `POST /connection/{id}/transfer`. Allowed for the owner only, full stop — no `connection:manage` fallback any more (removed 2026-09-04). An `edit` share does not reach delete either (docs/access-model.md §6).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/reconnect":{"post":{"operationId":"reconnectConnection","summary":"Reconnect connection","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["connection","connect_url"],"properties":{"connection":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (`conn_…`)."},"connector_slug":{"type":"string","description":"Which connector this account is on."},"name":{"type":"string"},"owner_user_id":{"type":"string","description":"Owning member (`usr_…`); calls run as this identity. Owner is implicit `edit`, plus delete and transfer — the two things no share confers. Move it with `POST /connection/{id}/transfer`. This is who is ACCOUNTABLE for the connection now, which after a transfer is not who authenticated it — see `connected_by_user_id`."},"connected_by_user_id":{"type":["string","null"],"description":"Who AUTHENTICATED this account (`usr_…`) — the member whose third-party credential the vault actually holds. Written once at creation and never rewritten. Distinct from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and deliberately does NOT touch the vault account, so after a transfer these are two different people and only this one may be rendered as \"Connected by\". **Null means NOT RECORDED, never \"nobody\"** — every connection created before this field existed is null, because nothing server-side could tell a never-transferred row (where `owner_user_id` would have been the honest answer) from a transferred one, and a fabricated attribution is worse than an absent one. Render nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id`, resolved server-side one batch per page (name, else email). Present on every branch that returns a connection, commands included. A null HERE beside a NON-null `connected_by_user_id` is a fact about the person, not a failed lookup: they have left the organization, and clients render \"Former member\" — the same word the `GET /audit-log` actor summary uses. Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"saffron_account_id":{"type":["string","null"],"description":"Vault account holding the credential. Null until the connect flow completes."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected"],"description":"`pending` = created but not yet authenticated (open `connect_url`); `active` = usable; `needs_reauth` = the credential expired or was revoked upstream, call reconnect; `disconnected` = no longer usable. Refreshed lazily against the vault when connections are read."},"last_error":{"type":["string","null"]},"access":{"type":"string","description":"The caller's relationship to this connection: `owner`, or a granted `view`/`use`/`edit` level. There is no oversight fallback for connections — this is never `\"oversight\"`; a connection the caller neither owns nor holds a grant on is not visible to them at all, whatever organization permission they hold."},"owner":{"type":["object","null"],"description":"Owner summary. Currently never populated on a connection response — no list route resolves it today — but kept in the schema in case a future route does; read `owner_user_id` plus a member lookup instead.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"shares":{"type":"array","description":"Present only when the caller may see the ACL: owner or `edit` access. A `view`/`use` recipient is never told who else the connection is shared with. Absent (not empty) when the caller may not see it.","items":{"$ref":"#/components/schemas/AclEntry"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"can_share":{"type":"boolean","description":"Whether the caller may share this connection — owner, `edit` access, or a team admin on a team-granted one. No `connection:manage` fallback. Computed server-side (CLAUDE.md's API-first rule); the console reads this rather than re-deriving it from `owner_user_id`/permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings — owner, or `edit` access. No `connection:manage` fallback. Computed server-side; same `can_manage` name used by toolbox and connector for the identical capability."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection — owner, full stop. No `connection:manage` fallback. Deliberately narrower than `can_manage`: an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, deliberately not an alias for `can_manage`: an `edit` grantee whose role omits `connection:share` (Auditor, Guest, any custom role) can reconfigure or reconnect the connection but was never meant to grant or revoke someone else's access to it, so `can_manage: true` on that row must not imply this control too. Mirrors `DELETE /connection/{id}/share/{aclId}`'s own gate exactly. Present on every list and detail row."},"access_summary":{"type":"object","properties":{"org_level":{"type":["string","null"],"enum":["view","use",null]},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}},"description":"Present only when `can_share` is true. Bounded by design — page `GET /connection/{id}/share` for the grants themselves."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this connection — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the connection reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this connection through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},"connect_url":{"type":"string","format":"uri","description":"Where the USER authenticates with the provider. Hand it to them as a link — Elaichi never accepts a password or API key for a third-party system through this API. Single-use and short-lived; mint another with reconnect. On completion the provider returns to `GET /connection/{id}/callback` and the status becomes `active`."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Mints a fresh `connect_url` for an existing connection — the fix for `needs_reauth`. The connection id and every toolbox entry pinned to it survive, so nothing has to be rebuilt; the new account replaces the old binding through the same attribution-checked callback, and the stale vault entry is dropped. The body is optional: an empty POST returns the user to the app root.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"redirect_uri":{"type":"string","maxLength":2000,"description":"App-relative return path."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/refresh-credentials":{"post":{"operationId":"refreshConnectionCredentials","summary":"Refresh connection credentials","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["refreshed_at","status"],"properties":{"refreshed_at":{"type":"string","format":"date-time"},"status":{"type":"string","description":"The row's status after the refresh (`active`)."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Forces Saffron to mint a fresh access token for this connection, in place — maintenance for a connection that is still healthy, where reconnect is the repair for one that is not. No body. Same mutation bar as reconnect, and only meaningful on a bound, `active` row whose `authentication_method` is refreshable (`oauth2`, `oauth2_client_credentials`, `jwt_bearer`) — exactly what the row's `can_refresh_credentials` reports, so render controls on that field. Everything else answers `409`: `not_refreshable` (Saffron is not called at all), `needs_reauth` (the provider rejected the refresh; the row is written to `needs_reauth` before this reply), or `connection_disconnected`. The `200` is a receipt, never a payload — the credential itself stays in the vault.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/run-post-install":{"post":{"operationId":"runConnectionPostInstall","summary":"Re-run connector post-install steps","tags":["Connections"],"responses":{"202":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["started"],"properties":{"started":{"type":"boolean"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Re-runs the connector's declared post-install steps (identity resolution, provider-side setup) against this connection — the repair for `post_install_error`, and the way to re-apply setup after a variable was corrected. No body. Same mutation bar as reconnect; the detail read's `can_run_post_install` is the render gate. Answers `202 { \"started\": true }`, never `200`: Saffron runs the steps detached, so the real outcome lands on the row (`status`/`last_error`) through a later read — re-fetch on the user's own refresh rather than sleeping and calling the result an answer. `409` when the connector declares no steps (`no_post_install`), the connection is unbound (`not_bound`), or Saffron no longer holds the account (`connection_disconnected`).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/variables":{"get":{"operationId":"listConnectionVariables","summary":"List connection variables","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","required":["name","value","is_secret","has_secrets"],"properties":{"name":{"type":"string","description":"Top-level context key; rows sort and page over it."},"value":{"description":"The customer-set value in its real shape — object, array or scalar. `null` when `is_secret` is true; inside objects, withheld leaves are absent rather than nulled or masked, so absence is the only shape that cannot lie."},"is_secret":{"type":"boolean","description":"Saffron's verdict, not a key-name guess; the value ships as `null`."},"has_secrets":{"type":"boolean","description":"A leaf at or under this key was withheld — true for a secret key itself, and for a public object one of whose leaves was stripped."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The connection's context variables — the setup values the customer supplied when they connected the account (subdomain, region, instance URL), one row per top-level key, plus one valueless row for every value Saffron encrypted away. Gated on `can_manage`, not the read bar: a `view`/`use` grantee runs tools through the connection, but the answers its owner typed into the connect form are not part of that — a caller without it gets `403`, deliberately not an empty `200`. An unbound connection answers an empty list. `can_manage` here is owner-or-real-edit-grant only, same as everywhere else — no `connection:manage` fallback any more (removed 2026-09-04).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"patch":{"operationId":"updateConnectionVariables","summary":"Update connection variables","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","required":["name","value","is_secret","has_secrets"],"properties":{"name":{"type":"string","description":"Top-level context key; rows sort and page over it."},"value":{"description":"The customer-set value in its real shape — object, array or scalar. `null` when `is_secret` is true; inside objects, withheld leaves are absent rather than nulled or masked, so absence is the only shape that cannot lie."},"is_secret":{"type":"boolean","description":"Saffron's verdict, not a key-name guess; the value ships as `null`."},"has_secrets":{"type":"boolean","description":"A leaf at or under this key was withheld — true for a secret key itself, and for a public object one of whose leaves was stripped."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Merge patch over top-level keys, same `can_manage` bar as the read: `set` replaces the keys it names, `unset` deletes the keys it names, and everything else in the account context — the keys this API may not read, the OAuth token blob first among them — is left where it is, so a patch cannot destroy what it cannot see. Secret keys cannot be set or unset here. Answers `200` with the same list envelope the GET returns, reflecting the new state.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"set":{"type":"object","description":"Top-level keys to replace, real JSON shapes as values.","additionalProperties":true},"unset":{"type":"array","items":{"type":"string"},"description":"Top-level keys to delete."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]}},"/connection/{id}/tools":{"get":{"operationId":"listConnectionTools","summary":"List tools for one connection","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Exact tool name — what a toolbox entry's `tool_name` must contain."},"description":{"type":"string"},"resource":{"type":"string"},"method":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true,"description":"JSON Schema for the tool arguments."},"restricted":{"type":"boolean","description":"True when the caller's restrictions block THIS tool. Blocked tools are returned flagged, never omitted — a shorter list is no signal, because nobody knows the length it should have been. Presence is not permission: execution still refuses it. The connector-level 403 is a separate question and is unchanged."},"restricted_by":{"type":["string","null"],"enum":["role","user",null],"description":"Which precedence layer's rule blocks this tool, null when none does. Same field, union and meaning as on connector and connection rows: adds *which* to `restricted`'s *whether* and nothing else — no rule id, author, reason or coverage. `restricted === (restricted_by !== null)` always."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"What this authenticated account can run: the connector's tools, each flagged `restricted`/`restricted_by` for the caller rather than filtered out — this is where a PARTIALLY restricted connection says so, since the connection row's own `restricted_by` asserts the whole connector is blocked. Contrast `GET /connector/{slug}/tools`, which is the catalog capability with no account behind it. A restricted connector answers 403. Resolves the same access as `GET /connection/{id}`: a connection the caller holds no grant on is `404`, with no oversight opt-in of any kind — a `visibility` query parameter is not accepted.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/transfer":{"post":{"operationId":"transferConnection","summary":"Transfer connection ownership","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (`conn_…`)."},"connector_slug":{"type":"string","description":"Which connector this account is on."},"name":{"type":"string"},"owner_user_id":{"type":"string","description":"Owning member (`usr_…`); calls run as this identity. Owner is implicit `edit`, plus delete and transfer — the two things no share confers. Move it with `POST /connection/{id}/transfer`. This is who is ACCOUNTABLE for the connection now, which after a transfer is not who authenticated it — see `connected_by_user_id`."},"connected_by_user_id":{"type":["string","null"],"description":"Who AUTHENTICATED this account (`usr_…`) — the member whose third-party credential the vault actually holds. Written once at creation and never rewritten. Distinct from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and deliberately does NOT touch the vault account, so after a transfer these are two different people and only this one may be rendered as \"Connected by\". **Null means NOT RECORDED, never \"nobody\"** — every connection created before this field existed is null, because nothing server-side could tell a never-transferred row (where `owner_user_id` would have been the honest answer) from a transferred one, and a fabricated attribution is worse than an absent one. Render nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id`, resolved server-side one batch per page (name, else email). Present on every branch that returns a connection, commands included. A null HERE beside a NON-null `connected_by_user_id` is a fact about the person, not a failed lookup: they have left the organization, and clients render \"Former member\" — the same word the `GET /audit-log` actor summary uses. Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"saffron_account_id":{"type":["string","null"],"description":"Vault account holding the credential. Null until the connect flow completes."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected"],"description":"`pending` = created but not yet authenticated (open `connect_url`); `active` = usable; `needs_reauth` = the credential expired or was revoked upstream, call reconnect; `disconnected` = no longer usable. Refreshed lazily against the vault when connections are read."},"last_error":{"type":["string","null"]},"access":{"type":"string","description":"The caller's relationship to this connection: `owner`, or a granted `view`/`use`/`edit` level. There is no oversight fallback for connections — this is never `\"oversight\"`; a connection the caller neither owns nor holds a grant on is not visible to them at all, whatever organization permission they hold."},"owner":{"type":["object","null"],"description":"Owner summary. Currently never populated on a connection response — no list route resolves it today — but kept in the schema in case a future route does; read `owner_user_id` plus a member lookup instead.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"shares":{"type":"array","description":"Present only when the caller may see the ACL: owner or `edit` access. A `view`/`use` recipient is never told who else the connection is shared with. Absent (not empty) when the caller may not see it.","items":{"$ref":"#/components/schemas/AclEntry"}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"can_share":{"type":"boolean","description":"Whether the caller may share this connection — owner, `edit` access, or a team admin on a team-granted one. No `connection:manage` fallback. Computed server-side (CLAUDE.md's API-first rule); the console reads this rather than re-deriving it from `owner_user_id`/permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings — owner, or `edit` access. No `connection:manage` fallback. Computed server-side; same `can_manage` name used by toolbox and connector for the identical capability."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection — owner, full stop. No `connection:manage` fallback. Deliberately narrower than `can_manage`: an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, deliberately not an alias for `can_manage`: an `edit` grantee whose role omits `connection:share` (Auditor, Guest, any custom role) can reconfigure or reconnect the connection but was never meant to grant or revoke someone else's access to it, so `can_manage: true` on that row must not imply this control too. Mirrors `DELETE /connection/{id}/share/{aclId}`'s own gate exactly. Present on every list and detail row."},"access_summary":{"type":"object","properties":{"org_level":{"type":["string","null"],"enum":["view","use",null]},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}},"description":"Present only when `can_share` is true. Bounded by design — page `GET /connection/{id}/share` for the grants themselves."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this connection — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the connection reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this connection through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}},"affected_toolbox_count":{"type":"integer","description":"Exact number of toolboxes with an entry pinned to this connection."},"broken_delegation_count":{"type":"integer","description":"Exact number of those that stopped working, because the outgoing owner pinned them and lost the access they run on. Already 0 when any grant saved them — the `retain_access` one, or an org-wide/team share the outgoing owner already held."},"affected_toolboxes":{"type":"array","description":"At most five, breaking ones first. A preview — read the counts, not this length.","items":{"type":"object","properties":{"toolbox_id":{"type":"string"},"toolbox_name":{"type":"string"},"kind":{"type":"string","enum":["toolbox"]},"breaks_without_retained_access":{"type":"boolean","description":"The outgoing owner pinned at least one of this toolbox's entries on the connection."}}}},"retained_access":{"type":"boolean","description":"True iff a retained `use` grant was actually made for the outgoing owner. False when they never asked for one — which is not the same as having lost access, since a grant they already held still counts: read `access` for that."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Changes `owner_user_id` — and nothing else. Transfer is one thing only: the new owner gets implicit `edit`, plus delete and transfer, which no grant confers; every existing grant on the connection is left exactly as it was, so who else could already reach it is unaffected. The removed `owner_scope`/`team_id` body is gone along with the `scope` it moved between — to change who can reach a connection, use `POST /connection/{id}/share` and `DELETE /connection/{id}/share/{aclId}` instead. Only the Elaichi row changes — the vaulted account is untouched, so toolbox entries pinned to this connection keep resolving and executing across the transfer. This is the non-destructive alternative to delete when a member leaves. Allowed for the current owner only — no `connection:manage` fallback any more (removed 2026-09-04). What it DOES cost the outgoing owner: their reach came from `owner_user_id`, not a grant, so unless a grant of their own already covered them they hold nothing afterwards (`access: null`) — and because a toolbox entry runs on its DELEGATOR's standing on the pinned connection, every entry THEY pinned stops resolving, for everyone including the new owner. `retain_access: true` prevents exactly that by granting them `use` as an ordinary, listed, revocable share in the same operation; only the connection's real owner may use it. Read `GET /connection/{id}/transfer-preview` first to see the same numbers before committing. **`connected_by_user_id` is NOT rewritten**, and that is the point: the vaulted credential still belongs to whoever authorized it, so the response carries the new `owner_user_id` beside the unchanged `connected_by_user_id`/`connected_by_label`. A client that renders \"Connected by\" off `owner_user_id` will claim the new owner authenticated an account they never touched. The new owner must be an ACTIVE member: a suspended one is refused with `400 member_not_active`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner_user_id"],"properties":{"owner_user_id":{"type":"string","description":"New owning member (`usr_…`)."},"retain_access":{"type":"boolean","description":"Keep the OUTGOING owner's access as an ordinary `use` share, so the toolbox entries they pinned to this connection keep working. Defaults to false. Refused (403) for any caller who is not the connection's own owner — offboarding never passes it, and a departing member keeps nothing."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/transfer-preview":{"get":{"operationId":"previewConnectionTransfer","summary":"Preview what a connection transfer would touch","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"affected_toolbox_count":{"type":"integer","description":"Exact number of toolboxes with an entry pinned to this connection."},"broken_delegation_count":{"type":"integer","description":"Exact number of the above that the CALLER pinned, and so the number at risk if they end up below `use`. Not narrowed by `keeps_access_without_retaining` — read the two together."},"affected_toolboxes":{"type":"array","description":"At most five, breaking ones first. A preview — read the counts, not this length.","items":{"type":"object","properties":{"toolbox_id":{"type":"string"},"toolbox_name":{"type":"string"},"kind":{"type":"string","enum":["toolbox"]},"breaks_without_retained_access":{"type":"boolean","description":"The outgoing owner pinned at least one of this toolbox's entries on the connection."}}}},"keeps_access_without_retaining":{"type":"boolean","description":"The caller already reaches this connection through a grant of their own (org-wide, or a team they are in), so handing it over costs them nothing even without `retain_access` and nothing they pinned breaks. Computed server-side because `access: \"owner\"` outranks and hides whatever grant is underneath it — a client cannot derive this."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"What `POST /connection/{id}/transfer` would break, answered BEFORE anything is written, so a client can put the consequence in front of somebody while the decision is still theirs. A bounded SUMMARY, not a list: exact server-computed counts plus at most five named toolboxes, breaking ones first — which is why it takes no `limit`/`cursor`. Gated on exactly what the transfer itself is gated on, so it can never advertise a door that 403s.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/share":{"get":{"operationId":"listConnectionShares","summary":"List connection ACL shares","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"ACL id (`acl_…`) — what `DELETE …/share/{aclId}` takes."},"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"],"description":"Null for org-wide grants."},"level":{"type":"string","enum":["view","use"]},"grantee":{"type":"object","description":"Resolved display data, so a page of grants needs no follow-up lookups.","properties":{"name":{"type":["string","null"]},"email":{"type":["string","null"]},"member_count":{"type":["integer","null"],"description":"Team grants only: how many people the grant reaches."}}},"created_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Cursor-paginated, and the only place the grant list is served — a connection may be shared with every member of the organization, so nothing else embeds it. Each entry carries a resolved `grantee` (name, email, and for teams a member count), so rendering a page needs no per-row lookups. Filter with `grantee_type`, search member names/emails and team names with `q`. Requires being able to share the connection (owner, `edit` access, or a team admin on a team-granted one) — no `connection:manage` fallback any more (removed 2026-09-04): a bare permission, with no real grant of the caller's own, no longer opens this even for a connection already shared with someone else.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Match member name or email, or team name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"grantee_type","in":"query","required":false,"description":"Restrict to one kind of grantee.","schema":{"type":"string","enum":["user","team","org"]}}]},"post":{"operationId":"shareConnection","summary":"Share a connection","tags":["Connections"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Grants one member, team, or the whole org `view` or `use` access to this connection. Sharing never changes `owner_user_id` — that only moves through `POST /connection/{id}/transfer` (docs/access-model.md §2). `use` is what lets the grantee bind this connection into their own toolbox entries and run tools through it. `edit` is not a valid level here: renaming, deleting or transferring a connection stays with the owner alone, never with a share (and never with `connection:manage` any more either, removed 2026-09-04). A private connection (no existing grants) may only be shared by its own owner — no organization permission reaches another member's private connection, sharing included; the owner opting people in is the one deliberate way in. Requires `edit` access or ownership, and `connection:share`; for a connection granted to a team, that team's admin also qualifies.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["grantee_type","level"],"properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"],"description":"User id (`usr_…`) or team id (`team_…`). Omitted or null for an org-wide grant."},"level":{"type":"string","enum":["view","use"]}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/share/{aclId}":{"delete":{"operationId":"revokeConnectionShare","summary":"Revoke connection share","tags":["Connections"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The grantee loses access immediately. The connection itself, its owner, and everything already running through it are untouched. Requires `can_revoke_share` (`can_share`, i.e. `edit` access plus `connection:share`) — no `connection:manage` fallback any more (removed 2026-09-04): a bare permission, with no real grant of the caller's own, no longer revokes a share on a connection shared with someone else.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."},{"name":"aclId","in":"path","required":true,"schema":{"type":"string"},"description":"ACL entry id from the share list."}]}},"/template":{"get":{"operationId":"listTemplates","summary":"List templates visible to the caller","tags":["Templates"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Template id (`tpl_…`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`)."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Templates carry no organization-wide oversight path — an org owner/admin sees a template only when they own it or it has been shared with them, same as any other member."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, bounded — the console renders it as a stack of connector logos, exactly as on a toolbox row. Present on every `GET /template` row AND on every command response (`POST /template`, `GET /template/{id}`, `PATCH /template/{id}`, `POST /template/{id}/transfer`), so a client that merges a command response into its list does not lose the stack. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the template."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /template` row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Same gate and same reasoning as the toolbox row's.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"Every grant, unabridged. Present only on `GET /template/{id}` when the caller may see the ACL (owner or `edit` access); list rows carry the bounded `access_summary` instead.","items":{"$ref":"#/components/schemas/AclEntry"}},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this template — owner, `edit` access, or `template:share`. No `template:manage` fallback. Mirrors `POST /template/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this template's own settings — owner, or `edit` access. No `template:manage` fallback. Mirrors `PATCH /template/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this template — ownership, full stop. No `template:manage` fallback. Mirrors `POST /template/{id}/transfer` and `DELETE /template/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /template/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this template — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the template reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this template through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Templates the caller owns plus every template shared with them, each with the caller's `access_level`. No organization permission widens this — templates carry no oversight path: an org owner/admin sees exactly the same rows as anyone else. `?q=` matches the name as a substring. Cursor-paginated.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Substring match (case-insensitive) on the template name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]},"post":{"operationId":"createTemplate","summary":"Create a template","tags":["Templates"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Template id (`tpl_…`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`)."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Templates carry no organization-wide oversight path — an org owner/admin sees a template only when they own it or it has been shared with them, same as any other member."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, bounded — the console renders it as a stack of connector logos, exactly as on a toolbox row. Present on every `GET /template` row AND on every command response (`POST /template`, `GET /template/{id}`, `PATCH /template/{id}`, `POST /template/{id}/transfer`), so a client that merges a command response into its list does not lose the stack. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the template."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /template` row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Same gate and same reasoning as the toolbox row's.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"Every grant, unabridged. Present only on `GET /template/{id}` when the caller may see the ACL (owner or `edit` access); list rows carry the bounded `access_summary` instead.","items":{"$ref":"#/components/schemas/AclEntry"}},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this template — owner, `edit` access, or `template:share`. No `template:manage` fallback. Mirrors `POST /template/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this template's own settings — owner, or `edit` access. No `template:manage` fallback. Mirrors `PATCH /template/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this template — ownership, full stop. No `template:manage` fallback. Mirrors `POST /template/{id}/transfer` and `DELETE /template/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /template/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this template — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the template reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this template through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Entry id (`tple_…`) — stable across updates only if you send it back unchanged."},"type":{"type":"string","enum":["proxy","synthetic"]},"synthetic_tool_id":{"type":["string","null"],"description":"Set for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug for `proxy` entries; null for synthetic."},"tool_name":{"type":["string","null"],"description":"Connector tool name for `proxy` entries."},"overrides":{"type":"object","description":"Presentation overrides — see the entry input schema for the same shape, request-side.","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"defaults":{"type":"object","additionalProperties":true}}},"frozen_params":{"type":"object","additionalProperties":true},"enabled":{"type":"boolean"}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `template:create`. Entries are validated against the connector catalog and the caller's restrictions as a set — one bad name rejects the whole call. `shares` grants access at creation time and additionally needs `template:share`; a template never references a connection, so — unlike toolbox creation — there is nothing to warn about in the response.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":4000},"entries":{"type":"array","maxItems":200,"items":{"type":"object","description":"One tool in the template. A `proxy` entry names a connector tool (`connector_slug` + `tool_name`); a `synthetic` entry names a `synthetic_tool_id` instead. Never a `connection_id` — a template pins nothing; sending one is a 400.","properties":{"type":{"type":"string","enum":["proxy","synthetic"],"description":"Defaults to `proxy`."},"synthetic_tool_id":{"type":["string","null"],"description":"Required for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":["string","null"],"maxLength":200,"description":"Exact tool name from `GET /connector/{slug}/tools`."},"overrides":{"type":"object","description":"How the tool is presented to the model. Each key REPLACES the derived value.","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"type":"string","maxLength":4000},"input_schema":{"type":"object","additionalProperties":true,"description":"Must be a JSON Schema object with `\"type\": \"object\"`. Replaces the derived schema entirely."},"defaults":{"type":"object","additionalProperties":true,"description":"Default argument values the caller may still override."}}},"frozen_params":{"type":"object","additionalProperties":true,"description":"Arguments pinned by the author. Unlike `overrides.defaults`, a caller cannot change these."},"enabled":{"type":"boolean"}}}},"shares":{"type":"array","maxItems":50,"description":"Grants to create alongside the resource. Omit, or send `[]`, for a private one. A non-empty array additionally needs `<resource>:share`.","items":{"$ref":"#/components/schemas/ShareRequest"}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/template/{id}":{"get":{"operationId":"getTemplate","summary":"Get a template","tags":["Templates"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Template id (`tpl_…`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`)."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Templates carry no organization-wide oversight path — an org owner/admin sees a template only when they own it or it has been shared with them, same as any other member."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, bounded — the console renders it as a stack of connector logos, exactly as on a toolbox row. Present on every `GET /template` row AND on every command response (`POST /template`, `GET /template/{id}`, `PATCH /template/{id}`, `POST /template/{id}/transfer`), so a client that merges a command response into its list does not lose the stack. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the template."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /template` row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Same gate and same reasoning as the toolbox row's.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"Every grant, unabridged. Present only on `GET /template/{id}` when the caller may see the ACL (owner or `edit` access); list rows carry the bounded `access_summary` instead.","items":{"$ref":"#/components/schemas/AclEntry"}},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this template — owner, `edit` access, or `template:share`. No `template:manage` fallback. Mirrors `POST /template/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this template's own settings — owner, or `edit` access. No `template:manage` fallback. Mirrors `PATCH /template/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this template — ownership, full stop. No `template:manage` fallback. Mirrors `POST /template/{id}/transfer` and `DELETE /template/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /template/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this template — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the template reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this template through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Entry id (`tple_…`) — stable across updates only if you send it back unchanged."},"type":{"type":"string","enum":["proxy","synthetic"]},"synthetic_tool_id":{"type":["string","null"],"description":"Set for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug for `proxy` entries; null for synthetic."},"tool_name":{"type":["string","null"],"description":"Connector tool name for `proxy` entries."},"overrides":{"type":"object","description":"Presentation overrides — see the entry input schema for the same shape, request-side.","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"defaults":{"type":"object","additionalProperties":true}}},"frozen_params":{"type":"object","additionalProperties":true},"enabled":{"type":"boolean"}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Includes the full entry list (never a `connection_id`), and `access_summary` when the caller may see the ACL. Resolves docs/access-model.md §4 ONLY — owner or an applicable grant — and a template neither reaches is `404`, never `403`, for everyone including a holder of `template:view`/`template:manage`. Templates carry no oversight fallback: unlike toolboxes and connections, there is no `?visibility=org` opt-in here.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template id (`tpl_…`)."}]},"patch":{"operationId":"updateTemplate","summary":"Update a template","tags":["Templates"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Template id (`tpl_…`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`)."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Templates carry no organization-wide oversight path — an org owner/admin sees a template only when they own it or it has been shared with them, same as any other member."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, bounded — the console renders it as a stack of connector logos, exactly as on a toolbox row. Present on every `GET /template` row AND on every command response (`POST /template`, `GET /template/{id}`, `PATCH /template/{id}`, `POST /template/{id}/transfer`), so a client that merges a command response into its list does not lose the stack. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the template."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /template` row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Same gate and same reasoning as the toolbox row's.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"Every grant, unabridged. Present only on `GET /template/{id}` when the caller may see the ACL (owner or `edit` access); list rows carry the bounded `access_summary` instead.","items":{"$ref":"#/components/schemas/AclEntry"}},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this template — owner, `edit` access, or `template:share`. No `template:manage` fallback. Mirrors `POST /template/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this template's own settings — owner, or `edit` access. No `template:manage` fallback. Mirrors `PATCH /template/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this template — ownership, full stop. No `template:manage` fallback. Mirrors `POST /template/{id}/transfer` and `DELETE /template/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /template/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this template — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the template reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this template through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Entry id (`tple_…`) — stable across updates only if you send it back unchanged."},"type":{"type":"string","enum":["proxy","synthetic"]},"synthetic_tool_id":{"type":["string","null"],"description":"Set for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug for `proxy` entries; null for synthetic."},"tool_name":{"type":["string","null"],"description":"Connector tool name for `proxy` entries."},"overrides":{"type":"object","description":"Presentation overrides — see the entry input schema for the same shape, request-side.","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"defaults":{"type":"object","additionalProperties":true}}},"frozen_params":{"type":"object","additionalProperties":true},"enabled":{"type":"boolean"}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"**`entries` REPLACES the entire entry list when present** — omitted entries are deleted. Omitting `entries` altogether leaves them untouched; `name`/`description` are ordinary merge-patch fields. Toolboxes already stamped from this template are UNTOUCHED (stamping copies entries once). Requires `edit` access or ownership (own grant) — no `template:manage` fallback.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":4000},"entries":{"type":"array","maxItems":200,"items":{"type":"object","description":"One tool in the template. A `proxy` entry names a connector tool (`connector_slug` + `tool_name`); a `synthetic` entry names a `synthetic_tool_id` instead. Never a `connection_id` — a template pins nothing; sending one is a 400.","properties":{"type":{"type":"string","enum":["proxy","synthetic"],"description":"Defaults to `proxy`."},"synthetic_tool_id":{"type":["string","null"],"description":"Required for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":["string","null"],"maxLength":200,"description":"Exact tool name from `GET /connector/{slug}/tools`."},"overrides":{"type":"object","description":"How the tool is presented to the model. Each key REPLACES the derived value.","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"type":"string","maxLength":4000},"input_schema":{"type":"object","additionalProperties":true,"description":"Must be a JSON Schema object with `\"type\": \"object\"`. Replaces the derived schema entirely."},"defaults":{"type":"object","additionalProperties":true,"description":"Default argument values the caller may still override."}}},"frozen_params":{"type":"object","additionalProperties":true,"description":"Arguments pinned by the author. Unlike `overrides.defaults`, a caller cannot change these."},"enabled":{"type":"boolean"}}},"description":"Replaces every entry."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template id (`tpl_…`)."}]},"delete":{"operationId":"deleteTemplate","summary":"Delete a template","tags":["Templates"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible: the definition, its entries and every share are gone. Toolboxes already stamped from it keep their own, independent entries — `template_id` on those rows is provenance only and may now dangle. Requires ownership — no `template:manage` fallback.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template id (`tpl_…`)."}]}},"/template/{id}/transfer":{"post":{"operationId":"transferTemplate","summary":"Transfer template ownership","tags":["Templates"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Template id (`tpl_…`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`)."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Templates carry no organization-wide oversight path — an org owner/admin sees a template only when they own it or it has been shared with them, same as any other member."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, bounded — the console renders it as a stack of connector logos, exactly as on a toolbox row. Present on every `GET /template` row AND on every command response (`POST /template`, `GET /template/{id}`, `PATCH /template/{id}`, `POST /template/{id}/transfer`), so a client that merges a command response into its list does not lose the stack. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the template."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /template` row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Same gate and same reasoning as the toolbox row's.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"Every grant, unabridged. Present only on `GET /template/{id}` when the caller may see the ACL (owner or `edit` access); list rows carry the bounded `access_summary` instead.","items":{"$ref":"#/components/schemas/AclEntry"}},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this template — owner, `edit` access, or `template:share`. No `template:manage` fallback. Mirrors `POST /template/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this template's own settings — owner, or `edit` access. No `template:manage` fallback. Mirrors `PATCH /template/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this template — ownership, full stop. No `template:manage` fallback. Mirrors `POST /template/{id}/transfer` and `DELETE /template/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /template/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this template — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the template reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this template through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Changes `owner_user_id` — and nothing else. Every existing grant is left exactly as it was. Allowed for the current owner only — no `template:manage` fallback.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner_user_id"],"properties":{"owner_user_id":{"type":"string","description":"New owning member (`usr_…`)."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template id (`tpl_…`)."}]}},"/template/{id}/share":{"get":{"operationId":"listTemplateShares","summary":"List template ACL shares","tags":["Templates"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"ACL id (`acl_…`) — what `DELETE …/share/{aclId}` takes."},"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"],"description":"Null for org-wide grants."},"level":{"type":"string","enum":["view","use","edit"]},"grantee":{"type":"object","description":"Resolved display data, so a page of grants needs no follow-up lookups.","properties":{"name":{"type":["string","null"]},"email":{"type":["string","null"]},"member_count":{"type":["integer","null"],"description":"Team grants only: how many people the grant reaches."}}},"created_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Cursor-paginated, and the only place the grant list is served. Each entry carries a resolved `grantee`. Filter with `grantee_type`, search member names/emails and team names with `q`. Requires owner or `edit` access (own grant) — no `template:manage` fallback.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template id (`tpl_…`)."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Match member name or email, or team name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"grantee_type","in":"query","required":false,"description":"Restrict to one kind of grantee.","schema":{"type":"string","enum":["user","team","org"]}}]},"post":{"operationId":"shareTemplate","summary":"Share a template","tags":["Templates"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AclEntry"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Grants one user, team, or the whole org access at a level. `use` is what lets the grantee stamp a toolbox from it (`POST /toolbox { template_id }`); `edit` additionally lets them change its entries and its own grants. Re-granting the same grantee replaces the level rather than stacking. Requires `edit` access or ownership, and `template:share`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareRequest"}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template id (`tpl_…`)."}]}},"/template/{id}/share/{aclId}":{"delete":{"operationId":"revokeTemplateShare","summary":"Revoke a template share","tags":["Templates"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The grantee loses access immediately — including the ability to stamp new toolboxes from it. Toolboxes already stamped are untouched. Requires `can_revoke_share` (`can_share` plus `edit` access) — no `template:manage` fallback.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template id (`tpl_…`)."},{"name":"aclId","in":"path","required":true,"schema":{"type":"string"},"description":"ACL entry id from the share list."}]}},"/toolbox":{"get":{"operationId":"listToolboxes","summary":"List toolboxes visible to the caller","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Toolbox id — a stored row (`tbx_…`), or a dynamic id (`global:{user_id}` / `connection:{connection_id}`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`). For a dynamic row, the caller."},"type":{"type":"string","enum":["stored","global","connection"],"description":"`stored` = a real ACL-backed row. `global`/`connection` = computed per request, always `readonly: true`."},"readonly":{"type":"boolean","description":"True for the dynamic global/per-connection toolboxes — never editable, shareable, transferable or deletable."},"template_id":{"type":["string","null"],"description":"Provenance only (stamped at creation, never a live link) — null for a from-scratch toolbox and for every dynamic row. May dangle after the template is deleted."},"connection_id":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"connector_slug":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Always `owner` for a dynamic row. Never `oversight` — toolboxes have no org-wide oversight fallback."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Cheap per-page SQL aggregate over entries with `connection_id IS NULL` — drives the console's Status column (\"Ready\" / \"N need connection\"). Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, bounded — the console renders it as a stack of connector logos. Present on every `GET /toolbox` row: a stored row summarises its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarises the connections it spans. Present on every command response too (`POST /toolbox`, `GET /toolbox/{id}`, `PATCH /toolbox/{id}`, `POST /toolbox/{id}/transfer`), so a client that merges one into its list does not show a just-created or just-edited toolbox as having no integrations. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the toolbox."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":["string","null"],"format":"date-time","description":"Null for dynamic rows, which are computed, never stored."},"updated_at":{"type":["string","null"],"format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /toolbox` stored row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Absent from dynamic rows, and deliberately absent for a `view`/`use` grantee: the grantee set, counts included, is information about colleagues (docs/access-model.md §8). Read `can_see_shares` to distinguish \"not permitted\" from \"not carried\"; never infer it from this field's absence.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"A bounded preview of the ACL (same cap as `access_summary`), present only on `GET /toolbox/{id}` when the caller may see the ACL. Never on a list row (`GET /toolbox`) — page `GET /toolbox/{id}/share` for the full, cursor-paginated grant list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a bounded rollup — present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see the ACL (the same gate `access_summary` uses). Never the full set: page `GET /toolbox/{id}/connected-app` for that.","properties":{"count":{"type":"integer"},"preview":{"type":"array","description":"At most 5 apps, for a hover preview.","items":{"type":"object","properties":{"grant_id":{"type":"string","description":"OAuth grant id (`ogrt_…`) — what `DELETE /oauth/grant/{id}` takes, scoped to the grant's own user."},"client_id":{"type":"string"},"client_name":{"type":["string","null"],"description":"Null when the OAuth client row is gone."},"user":{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":"string","description":"Omitted along with `email` when the id no longer resolves to an org member."},"email":{"type":"string","format":"email"}}},"via":{"type":"string","enum":["toolbox","all_tools"],"description":"`toolbox`: this toolbox is named explicitly on the grant. `all_tools`: an \"All my tools\" authorization by a user who can currently use this toolbox — computed live, not a stored fact."}}}}}},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this toolbox — owner, `edit` access, or `toolbox:share`. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this toolbox's own settings — owner, or `edit` access. No `toolbox:manage` fallback. Mirrors `PATCH /toolbox/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this toolbox — ownership, full stop. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/transfer` and `DELETE /toolbox/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /toolbox/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this toolbox — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the toolbox reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this toolbox through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Toolboxes the caller owns plus every toolbox shared with them, each with the caller's `access_level`, PLUS the two virtual dynamic rows (`global:{user_id}`, one `connection:{connection_id}` per active usable connection) appended to the LAST page only (the one whose `next_cursor` is null), always `access_level: \"owner\"` — so the order across the whole listing is stored toolboxes first, automatic ones after. There is no org-wide oversight listing for toolboxes and no `?visibility=` parameter: no organization permission — not even `toolbox:view`/`toolbox:manage` — widens this beyond what the caller owns or was granted; a private toolbox never appears here for anyone but its owner and its grantees. `?q=` matches the name as a substring. `?type=stored` drops the dynamic rows entirely — for a caller, such as a toolbox picker, that only wants ACL-backed rows and would otherwise have to filter the dynamic ones out itself. `?connector_slug=` narrows to the toolboxes holding a tool from any of the named connectors; take the slugs worth passing from `GET /toolbox/connector`, not from the whole catalog. The three filters combine by AND. Cursor-paginated.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Substring match (case-insensitive) on the toolbox name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"type","in":"query","required":false,"description":"`stored` restricts the listing to ACL-backed toolboxes, omitting the dynamic `global:…`/`connection:…` rows; `automatic` is the mirror, paging the dynamic rows alone. Unset returns both, stored first. Any other value is a `400`.","schema":{"type":"string","enum":["stored","automatic"]}},{"name":"connector_slug","in":"query","required":false,"description":"Filter to the toolboxes holding at least one tool from ANY of these connectors — a union, never an intersection. Repeat the parameter or send one comma-separated value; both are the same filter, spelled exactly as `GET /connection?connector_slug=` is. At most 25 distinct values per request; more is a `400`, never a silent truncation. A dynamic row is matched on the connectors it really spans: a `connection:{id}` row on its own connector, the `global:{user_id}` row when any of the caller's active usable connections matches. `GET /toolbox/connector` is where the values come from.","explode":true,"schema":{"type":"array","maxItems":25,"items":{"type":"string"}}}]},"post":{"operationId":"createToolbox","summary":"Create a toolbox — also the stamping endpoint","tags":["Toolboxes"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Toolbox id — a stored row (`tbx_…`), or a dynamic id (`global:{user_id}` / `connection:{connection_id}`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`). For a dynamic row, the caller."},"type":{"type":"string","enum":["stored","global","connection"],"description":"`stored` = a real ACL-backed row. `global`/`connection` = computed per request, always `readonly: true`."},"readonly":{"type":"boolean","description":"True for the dynamic global/per-connection toolboxes — never editable, shareable, transferable or deletable."},"template_id":{"type":["string","null"],"description":"Provenance only (stamped at creation, never a live link) — null for a from-scratch toolbox and for every dynamic row. May dangle after the template is deleted."},"connection_id":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"connector_slug":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Always `owner` for a dynamic row. Never `oversight` — toolboxes have no org-wide oversight fallback."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Cheap per-page SQL aggregate over entries with `connection_id IS NULL` — drives the console's Status column (\"Ready\" / \"N need connection\"). Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, bounded — the console renders it as a stack of connector logos. Present on every `GET /toolbox` row: a stored row summarises its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarises the connections it spans. Present on every command response too (`POST /toolbox`, `GET /toolbox/{id}`, `PATCH /toolbox/{id}`, `POST /toolbox/{id}/transfer`), so a client that merges one into its list does not show a just-created or just-edited toolbox as having no integrations. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the toolbox."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":["string","null"],"format":"date-time","description":"Null for dynamic rows, which are computed, never stored."},"updated_at":{"type":["string","null"],"format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /toolbox` stored row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Absent from dynamic rows, and deliberately absent for a `view`/`use` grantee: the grantee set, counts included, is information about colleagues (docs/access-model.md §8). Read `can_see_shares` to distinguish \"not permitted\" from \"not carried\"; never infer it from this field's absence.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"A bounded preview of the ACL (same cap as `access_summary`), present only on `GET /toolbox/{id}` when the caller may see the ACL. Never on a list row (`GET /toolbox`) — page `GET /toolbox/{id}/share` for the full, cursor-paginated grant list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a bounded rollup — present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see the ACL (the same gate `access_summary` uses). Never the full set: page `GET /toolbox/{id}/connected-app` for that.","properties":{"count":{"type":"integer"},"preview":{"type":"array","description":"At most 5 apps, for a hover preview.","items":{"type":"object","properties":{"grant_id":{"type":"string","description":"OAuth grant id (`ogrt_…`) — what `DELETE /oauth/grant/{id}` takes, scoped to the grant's own user."},"client_id":{"type":"string"},"client_name":{"type":["string","null"],"description":"Null when the OAuth client row is gone."},"user":{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":"string","description":"Omitted along with `email` when the id no longer resolves to an org member."},"email":{"type":"string","format":"email"}}},"via":{"type":"string","enum":["toolbox","all_tools"],"description":"`toolbox`: this toolbox is named explicitly on the grant. `all_tools`: an \"All my tools\" authorization by a user who can currently use this toolbox — computed live, not a stored fact."}}}}}},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this toolbox — owner, `edit` access, or `toolbox:share`. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this toolbox's own settings — owner, or `edit` access. No `toolbox:manage` fallback. Mirrors `PATCH /toolbox/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this toolbox — ownership, full stop. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/transfer` and `DELETE /toolbox/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /toolbox/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this toolbox — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the toolbox reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this toolbox through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Entry id (`tbxe_…`) — stable across updates only if you send it back unchanged."},"type":{"type":"string","enum":["proxy","synthetic"]},"synthetic_tool_id":{"type":["string","null"],"description":"Set for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug for `proxy` entries; null for synthetic."},"connection_id":{"type":["string","null"],"description":"`null` = \"needs connection\" — stamped-but-unfilled, or a pin that broke. Never set for synthetic entries."},"delegated_by_user_id":{"type":["string","null"],"description":"Server-stamped, never a client input: whose `use` grant on `connection_id` this pin rides on — the live authority chain (docs/access-model.md §6). If this user loses `use` on the connection, the entry goes unmet for every executor, not just them. Null iff `connection_id` is null."},"tool_name":{"type":["string","null"],"description":"Connector tool name for `proxy` entries."},"overrides":{"type":"object","description":"Presentation overrides — see the entry input schema for the same shape, request-side.","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"defaults":{"type":"object","additionalProperties":true}}},"frozen_params":{"type":"object","additionalProperties":true},"enabled":{"type":"boolean"},"connection_status":{"type":["string","null"],"enum":["pending","active","needs_reauth","disconnected",null],"description":"Detail responses only: the pinned connection's live status, when resolved."},"unmet":{"type":"boolean","description":"Detail responses only: no usable connection backs this entry — pin is empty, broken, or the delegator lost `use`."},"tool_available":{"type":"boolean","description":"Detail responses only: false when the catalog (or the synthetic-tool store) no longer has this tool."},"delegation_ok":{"type":"boolean","description":"Detail responses only: false when `connection_id` is set but `delegated_by_user_id` no longer holds `use` on it. Always true when `connection_id` is null."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"Detail responses only: HOW `delegated_by_user_id` holds `connection_id` — `accessVia` over the DELEGATOR, same owner/org/team/direct precedence (broadest wins) as the caller's own `access_via` (docs/access-model.md §8). `delegation_ok` says the delegation still stands; this says what it stands on, so copy can avoid implying a personal grant where an org-wide one is doing the work. Null for a synthetic entry (its steps, not the row, hold connections), for a pin with no connection or no stamped delegator, and whenever the delegation no longer works (`delegation_ok: false`) — the delegator is no longer an active member, or their reach has fallen below `use`."},"can_repin":{"type":"boolean","description":"Detail responses only, and the only field here that differs by READER: may the caller repair this entry with `POST /toolbox/{id}/repin`? True when the entry is broken (`delegation_ok: false` on a proxy entry that pins a connection), the caller holds `use` on that connection, and the caller holds `edit` (or ownership) on this toolbox. `delegation_ok: false` is the same answer for everybody who opens the toolbox; this says who among them can do something about it. Do not re-derive it — the inputs (the caller's grants and team memberships) are not on the wire."}}},"description":"Resolved entries — always present, at every access level."},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true,"description":"Present only with `?include=schemas` — omitted by default; a toolbox spanning many connections can carry thousands of complete JSON Schemas."},"type":{"type":"string","enum":["proxy","synthetic"]},"connector_slug":{"type":["string","null"],"description":"`\"synthetic\"` sentinel is never used here; null for synthetic tools."},"connections":{"type":"array","description":"Every account this tool can reach, labelled exactly as the `connection` argument enum in `input_schema` accepts them.","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"}}}},"connection_id":{"type":["string","null"],"description":"Set only when exactly one connection backs this tool (read `connections` otherwise). Null for synthetic tools."},"synthetic_tool_id":{"type":["string","null"]}}},"description":"The resolved, runnable tool surface. Present ONLY when the caller's access reaches `use` — a view-only grantee must not see delegated connections' labels."},"delegation":{"type":["object","null"],"description":"What this toolbox RUNS ON — the disclosure side of §6 delegation. Sharing a toolbox at `use` hands the grantee execution over every connection its entries reach (pinned directly, or reached through a synthetic entry's steps, where the synthetic tool's owner is the delegator), while leaving the connections themselves invisible to them. A bounded rollup, never the set: a toolbox pinning a thousand entries across hundreds of connections returns the same size response. Present on every stored-toolbox response — detail, create, update and share — and always empty for a dynamic row, which delegates nothing. On a create/update/share response it may instead be `null`: the write succeeded and the summary alone could not be computed (never \"nothing is delegated\", which is an all-zeroes summary) — re-read `GET /toolbox/{id}` for the disclosure. Unlike `tools`, this is NOT gated on `use`: its `visible` half already withholds every name the caller has no right to, and its two totals say only how many connections and how many people stand behind the toolbox — which a grantee can already count off `entries[].connection_id`.","properties":{"connection_count":{"type":"integer","description":"Distinct connections the tools run through, whether or not the caller can see them."},"delegator_count":{"type":"integer","description":"Distinct people whose access those pins ride on. Never named at this level."},"visible":{"type":"object","description":"The same delegation restricted to what THIS caller may already see (§4). Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection `GET /connection/{id}` would 404 for them.","properties":{"connection_count":{"type":"integer"},"private_connection_count":{"type":"integer","description":"Of the visible ones, how many carry no grants at all — only their owner can reach them today."},"preview":{"type":"array","description":"At most 3 of the visible connections, private ones first.","items":{"type":"object","properties":{"connection_id":{"type":"string"},"connector":{"type":"string"},"label":{"type":"string"},"via":{"type":"string","enum":["entry","synthetic_tool"],"description":"`entry` = an entry pins it directly. `synthetic_tool` = reached only through a synthetic tool's steps."},"delegated_by":{"type":"object","description":"Whose access this delegation rides on. `name`/`email` are absent for a user this org can no longer resolve. Read it together with `delegated_by_source`.","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}}},"delegated_by_source":{"type":"string","enum":["delegator","connection_owner"],"description":"`delegator` = the pin's own stamped delegator (or, for `via: synthetic_tool`, the tool's owner): this person really is who the execution rides on. `connection_owner` = the pin carries no delegator (an entry written before the column existed, never backfilled), so `delegated_by` is the connection's OWNER standing in as the delegator of record. Such a pin is still a real delegation and is still counted; copy that names a person must branch on this rather than claim they shared it."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"HOW `delegated_by` was entitled to this connection — `accessVia` over the DELEGATOR, with §8's own precedence: the BROADEST true source wins (owner, else org, else team, else direct) and the access level plays no part. `delegated_by` alone only says who pinned it; without this, copy reads an org-wide or team grant as a personal favour and hides how far the account actually reaches. Null whenever the delegation no longer works — the delegator is no longer an active member, or their reach over the connection has fallen below `use` — since the only honest statement left is who pinned it. Same predicate as the entry's own field (`delegationStanding`), so the two surfaces cannot disagree."},"delegated_by_access_via_team":{"type":"object","description":"The team behind `delegated_by_access_via: 'team'`, present ONLY when the CALLER is themselves a member of it — §8's bound verbatim, since a third party's team is a grantee of a connection the caller may not be able to open, and the grantee list stays behind `can_see_shares`. Absent otherwise; the copy then says \"a team\" without naming it.","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"private":{"type":"boolean"},"can_share":{"type":"boolean","description":"May the caller grant someone access to this connection directly — the same derivation `GET /connection/{id}`'s `can_share` and `POST /connection/{id}/share`'s own gate use."}}}}}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `toolbox:create`. Two ways to build one, combinable: pass `template_id` to STAMP it — copies that template's entries once, at creation time, never a live link; requires `use` access to the template — with `connection_map` (connector_slug -> connection_id) filling each stamped proxy entry's connector from one of the caller's own usable connections (the caller becomes that entry's delegator); a connector `connection_map` does not name is left `connection_id: null` (\"needs connection\") rather than rejected. And/or pass `entries` for hand-written ones beyond whatever stamping copied, validated the same way `PATCH /toolbox/{id}` validates them. `name` defaults to the template's name when stamping and is otherwise required. `shares` grants access at creation time and additionally needs `toolbox:share`; sharing DELEGATES (§6), which is what the response's `delegation` summary discloses — always present, not only when `shares` was sent, because the console opens \"Manage access\" straight off the row it just created and the disclosure has to be readable BEFORE that first grant.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Required unless template_id is set, where it defaults to the template's own name."},"description":{"type":["string","null"],"maxLength":4000},"template_id":{"type":"string","description":"Template id (`tpl_…`) to stamp from. Requires `use` access to it."},"connection_map":{"type":"object","additionalProperties":{"type":"string"},"description":"connector_slug -> connection_id (`conn_…`), filling stamped proxy entries. Ignored when template_id is absent."},"entries":{"type":"array","maxItems":200,"items":{"type":"object","description":"One tool in the toolbox. A `proxy` entry names a connector tool (`connector_slug` + `tool_name`), optionally pinned to a `connection_id`; a `synthetic` entry names a `synthetic_tool_id` instead.","properties":{"type":{"type":"string","enum":["proxy","synthetic"],"description":"Defaults to `proxy`."},"synthetic_tool_id":{"type":["string","null"],"description":"Required for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":["string","null"],"maxLength":200,"description":"Exact tool name from `GET /connector/{slug}/tools`."},"overrides":{"type":"object","description":"How the tool is presented to the model. Each key REPLACES the derived value.","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"type":"string","maxLength":4000},"input_schema":{"type":"object","additionalProperties":true,"description":"Must be a JSON Schema object with `\"type\": \"object\"`. Replaces the derived schema entirely."},"defaults":{"type":"object","additionalProperties":true,"description":"Default argument values the caller may still override."}}},"frozen_params":{"type":"object","additionalProperties":true,"description":"Arguments pinned by the author. Unlike `overrides.defaults`, a caller cannot change these."},"enabled":{"type":"boolean"},"connection_id":{"type":["string","null"],"description":"Pin this entry to one connection (`conn_…`) the ACTING EDITOR can use — they become its delegator (docs/access-model.md §6). `null` = \"needs connection.\" On `PATCH /toolbox/{id}`, an incoming entry whose (connection_id, tool_name) pair matches a pre-existing row keeps that row's original delegator rather than re-stamping the caller (the carry-forward rule) — only genuinely new pairs get the usability check and the caller's own stamp."}}}},"shares":{"type":"array","maxItems":50,"description":"Grants to create alongside the resource. Omit, or send `[]`, for a private one. A non-empty array additionally needs `<resource>:share`.","items":{"$ref":"#/components/schemas/ShareRequest"}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/toolbox/connector":{"get":{"operationId":"listToolboxConnectors","summary":"List the connectors used by the caller's toolboxes","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","required":["slug","name","logo","toolbox_count"],"properties":{"slug":{"type":"string","description":"Connector slug — the value to send as `GET /toolbox?connector_slug=`."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves, so an option always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null — including when the catalog could not be reached, which costs an option its picture and never its place in the list. Render initials from `name` when it is null."},"toolbox_count":{"type":"integer","description":"How many of the caller's visible toolboxes hold at least one tool from this connector, under the `?type=` in force. Always at least 1 — this endpoint never offers an option that filters to nothing."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The connectors that actually appear in the toolboxes `GET /toolbox` would return for this caller, and how many of those toolboxes hold each one — the options for `GET /toolbox?connector_slug=`, the same way `GET /connector/categories` is the options for `GET /connector?category=`. Build a connector filter on this rather than on `GET /connector`: the catalog offers connectors the caller has no toolbox for, and the empty list that follows selecting one reads as a bug rather than as an answer. **The guarantee:** filtering on any slug this endpoint returns yields exactly that row's `toolbox_count` toolboxes from `GET /toolbox` under the same `?type=` — so pass `?type=` here whenever you pass it there, or the facet describes a wider universe than the list will return. `toolbox_count` is a `COUNT(DISTINCT toolbox_id)` over the caller's WHOLE visible set, never over one page of it, so it is honest at any organization size. Ordered most-used first, slug ascending as the tiebreak. Cursor-paginated: the wire list is one row per connector the caller uses, and small-today is no reason to hand back a bare array. Visibility is docs/access-model.md §4 exactly, inherited from `GET /toolbox` — a connector reachable only through someone else's private toolbox is not here.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Anchored prefix match on the connector SLUG (`goo` matches `google-drive`), not on the display name: the name lives in the connector catalog, which the organization store cannot reach, so matching it would mean searching in the client over a paginated list. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"type","in":"query","required":false,"description":"Mirrors `GET /toolbox`'s `?type=` and must be given the same value: `stored` counts only ACL-backed toolboxes, `automatic` only the dynamic `global:…`/`connection:…` rows, unset both. Any other value is a `400`.","schema":{"type":"string","enum":["stored","automatic"]}}]}},"/toolbox/{id}":{"get":{"operationId":"getToolbox","summary":"Get a toolbox, resolved","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Toolbox id — a stored row (`tbx_…`), or a dynamic id (`global:{user_id}` / `connection:{connection_id}`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`). For a dynamic row, the caller."},"type":{"type":"string","enum":["stored","global","connection"],"description":"`stored` = a real ACL-backed row. `global`/`connection` = computed per request, always `readonly: true`."},"readonly":{"type":"boolean","description":"True for the dynamic global/per-connection toolboxes — never editable, shareable, transferable or deletable."},"template_id":{"type":["string","null"],"description":"Provenance only (stamped at creation, never a live link) — null for a from-scratch toolbox and for every dynamic row. May dangle after the template is deleted."},"connection_id":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"connector_slug":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Always `owner` for a dynamic row. Never `oversight` — toolboxes have no org-wide oversight fallback."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Cheap per-page SQL aggregate over entries with `connection_id IS NULL` — drives the console's Status column (\"Ready\" / \"N need connection\"). Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, bounded — the console renders it as a stack of connector logos. Present on every `GET /toolbox` row: a stored row summarises its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarises the connections it spans. Present on every command response too (`POST /toolbox`, `GET /toolbox/{id}`, `PATCH /toolbox/{id}`, `POST /toolbox/{id}/transfer`), so a client that merges one into its list does not show a just-created or just-edited toolbox as having no integrations. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the toolbox."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":["string","null"],"format":"date-time","description":"Null for dynamic rows, which are computed, never stored."},"updated_at":{"type":["string","null"],"format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /toolbox` stored row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Absent from dynamic rows, and deliberately absent for a `view`/`use` grantee: the grantee set, counts included, is information about colleagues (docs/access-model.md §8). Read `can_see_shares` to distinguish \"not permitted\" from \"not carried\"; never infer it from this field's absence.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"A bounded preview of the ACL (same cap as `access_summary`), present only on `GET /toolbox/{id}` when the caller may see the ACL. Never on a list row (`GET /toolbox`) — page `GET /toolbox/{id}/share` for the full, cursor-paginated grant list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a bounded rollup — present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see the ACL (the same gate `access_summary` uses). Never the full set: page `GET /toolbox/{id}/connected-app` for that.","properties":{"count":{"type":"integer"},"preview":{"type":"array","description":"At most 5 apps, for a hover preview.","items":{"type":"object","properties":{"grant_id":{"type":"string","description":"OAuth grant id (`ogrt_…`) — what `DELETE /oauth/grant/{id}` takes, scoped to the grant's own user."},"client_id":{"type":"string"},"client_name":{"type":["string","null"],"description":"Null when the OAuth client row is gone."},"user":{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":"string","description":"Omitted along with `email` when the id no longer resolves to an org member."},"email":{"type":"string","format":"email"}}},"via":{"type":"string","enum":["toolbox","all_tools"],"description":"`toolbox`: this toolbox is named explicitly on the grant. `all_tools`: an \"All my tools\" authorization by a user who can currently use this toolbox — computed live, not a stored fact."}}}}}},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this toolbox — owner, `edit` access, or `toolbox:share`. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this toolbox's own settings — owner, or `edit` access. No `toolbox:manage` fallback. Mirrors `PATCH /toolbox/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this toolbox — ownership, full stop. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/transfer` and `DELETE /toolbox/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /toolbox/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this toolbox — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the toolbox reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this toolbox through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Entry id (`tbxe_…`) — stable across updates only if you send it back unchanged."},"type":{"type":"string","enum":["proxy","synthetic"]},"synthetic_tool_id":{"type":["string","null"],"description":"Set for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug for `proxy` entries; null for synthetic."},"connection_id":{"type":["string","null"],"description":"`null` = \"needs connection\" — stamped-but-unfilled, or a pin that broke. Never set for synthetic entries."},"delegated_by_user_id":{"type":["string","null"],"description":"Server-stamped, never a client input: whose `use` grant on `connection_id` this pin rides on — the live authority chain (docs/access-model.md §6). If this user loses `use` on the connection, the entry goes unmet for every executor, not just them. Null iff `connection_id` is null."},"tool_name":{"type":["string","null"],"description":"Connector tool name for `proxy` entries."},"overrides":{"type":"object","description":"Presentation overrides — see the entry input schema for the same shape, request-side.","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"defaults":{"type":"object","additionalProperties":true}}},"frozen_params":{"type":"object","additionalProperties":true},"enabled":{"type":"boolean"},"connection_status":{"type":["string","null"],"enum":["pending","active","needs_reauth","disconnected",null],"description":"Detail responses only: the pinned connection's live status, when resolved."},"unmet":{"type":"boolean","description":"Detail responses only: no usable connection backs this entry — pin is empty, broken, or the delegator lost `use`."},"tool_available":{"type":"boolean","description":"Detail responses only: false when the catalog (or the synthetic-tool store) no longer has this tool."},"delegation_ok":{"type":"boolean","description":"Detail responses only: false when `connection_id` is set but `delegated_by_user_id` no longer holds `use` on it. Always true when `connection_id` is null."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"Detail responses only: HOW `delegated_by_user_id` holds `connection_id` — `accessVia` over the DELEGATOR, same owner/org/team/direct precedence (broadest wins) as the caller's own `access_via` (docs/access-model.md §8). `delegation_ok` says the delegation still stands; this says what it stands on, so copy can avoid implying a personal grant where an org-wide one is doing the work. Null for a synthetic entry (its steps, not the row, hold connections), for a pin with no connection or no stamped delegator, and whenever the delegation no longer works (`delegation_ok: false`) — the delegator is no longer an active member, or their reach has fallen below `use`."},"can_repin":{"type":"boolean","description":"Detail responses only, and the only field here that differs by READER: may the caller repair this entry with `POST /toolbox/{id}/repin`? True when the entry is broken (`delegation_ok: false` on a proxy entry that pins a connection), the caller holds `use` on that connection, and the caller holds `edit` (or ownership) on this toolbox. `delegation_ok: false` is the same answer for everybody who opens the toolbox; this says who among them can do something about it. Do not re-derive it — the inputs (the caller's grants and team memberships) are not on the wire."}}},"description":"Resolved entries — always present, at every access level."},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true,"description":"Present only with `?include=schemas` — omitted by default; a toolbox spanning many connections can carry thousands of complete JSON Schemas."},"type":{"type":"string","enum":["proxy","synthetic"]},"connector_slug":{"type":["string","null"],"description":"`\"synthetic\"` sentinel is never used here; null for synthetic tools."},"connections":{"type":"array","description":"Every account this tool can reach, labelled exactly as the `connection` argument enum in `input_schema` accepts them.","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"}}}},"connection_id":{"type":["string","null"],"description":"Set only when exactly one connection backs this tool (read `connections` otherwise). Null for synthetic tools."},"synthetic_tool_id":{"type":["string","null"]}}},"description":"The resolved, runnable tool surface. Present ONLY when the caller's access reaches `use` — a view-only grantee must not see delegated connections' labels."},"delegation":{"type":["object","null"],"description":"What this toolbox RUNS ON — the disclosure side of §6 delegation. Sharing a toolbox at `use` hands the grantee execution over every connection its entries reach (pinned directly, or reached through a synthetic entry's steps, where the synthetic tool's owner is the delegator), while leaving the connections themselves invisible to them. A bounded rollup, never the set: a toolbox pinning a thousand entries across hundreds of connections returns the same size response. Present on every stored-toolbox response — detail, create, update and share — and always empty for a dynamic row, which delegates nothing. On a create/update/share response it may instead be `null`: the write succeeded and the summary alone could not be computed (never \"nothing is delegated\", which is an all-zeroes summary) — re-read `GET /toolbox/{id}` for the disclosure. Unlike `tools`, this is NOT gated on `use`: its `visible` half already withholds every name the caller has no right to, and its two totals say only how many connections and how many people stand behind the toolbox — which a grantee can already count off `entries[].connection_id`.","properties":{"connection_count":{"type":"integer","description":"Distinct connections the tools run through, whether or not the caller can see them."},"delegator_count":{"type":"integer","description":"Distinct people whose access those pins ride on. Never named at this level."},"visible":{"type":"object","description":"The same delegation restricted to what THIS caller may already see (§4). Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection `GET /connection/{id}` would 404 for them.","properties":{"connection_count":{"type":"integer"},"private_connection_count":{"type":"integer","description":"Of the visible ones, how many carry no grants at all — only their owner can reach them today."},"preview":{"type":"array","description":"At most 3 of the visible connections, private ones first.","items":{"type":"object","properties":{"connection_id":{"type":"string"},"connector":{"type":"string"},"label":{"type":"string"},"via":{"type":"string","enum":["entry","synthetic_tool"],"description":"`entry` = an entry pins it directly. `synthetic_tool` = reached only through a synthetic tool's steps."},"delegated_by":{"type":"object","description":"Whose access this delegation rides on. `name`/`email` are absent for a user this org can no longer resolve. Read it together with `delegated_by_source`.","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}}},"delegated_by_source":{"type":"string","enum":["delegator","connection_owner"],"description":"`delegator` = the pin's own stamped delegator (or, for `via: synthetic_tool`, the tool's owner): this person really is who the execution rides on. `connection_owner` = the pin carries no delegator (an entry written before the column existed, never backfilled), so `delegated_by` is the connection's OWNER standing in as the delegator of record. Such a pin is still a real delegation and is still counted; copy that names a person must branch on this rather than claim they shared it."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"HOW `delegated_by` was entitled to this connection — `accessVia` over the DELEGATOR, with §8's own precedence: the BROADEST true source wins (owner, else org, else team, else direct) and the access level plays no part. `delegated_by` alone only says who pinned it; without this, copy reads an org-wide or team grant as a personal favour and hides how far the account actually reaches. Null whenever the delegation no longer works — the delegator is no longer an active member, or their reach over the connection has fallen below `use` — since the only honest statement left is who pinned it. Same predicate as the entry's own field (`delegationStanding`), so the two surfaces cannot disagree."},"delegated_by_access_via_team":{"type":"object","description":"The team behind `delegated_by_access_via: 'team'`, present ONLY when the CALLER is themselves a member of it — §8's bound verbatim, since a third party's team is a grantee of a connection the caller may not be able to open, and the grantee list stays behind `can_see_shares`. Absent otherwise; the copy then says \"a team\" without naming it.","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"private":{"type":"boolean"},"can_share":{"type":"boolean","description":"May the caller grant someone access to this connection directly — the same derivation `GET /connection/{id}`'s `can_share` and `POST /connection/{id}/share`'s own gate use."}}}}}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Includes the resolved entry list (always — even at `view` access, with `unmet`/`tool_available`/`delegation_ok`/`connection_status` computed live) and, only when the caller's access reaches `use`, the resolved `tools[]` a client reaching this toolbox through MCP would actually see — a view-only grantee must not see delegated connections' labels. `access_summary` and `connected_app_summary` are present when the caller may see the ACL — the second is the bounded rollup of OAuth-connected apps reaching this toolbox, paged in full by `GET /toolbox/{id}/connected-app`. Accepts a dynamic id (`global:…`/`connection:…`), always fully the caller's own to use. This resolves docs/access-model.md §4 ONLY — owner or an applicable grant — and a toolbox neither reaches is `404`, always, for everyone including a holder of `toolbox:view`/`toolbox:manage`: there is no oversight fallback and no `?visibility=` parameter for toolboxes, unlike connection/template/connector. `access` in the response is therefore always `owner`/`view`/`use`/`edit`, never `oversight`. `?include=schemas` opts into full JSON Schemas on `tools[].input_schema` — off by default, since a toolbox spanning many connections can carry thousands of complete schemas.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Toolbox id (`tbx_…`), or a dynamic id (`global:{user_id}` / `connection:{connection_id}`)."},{"name":"include","in":"query","required":false,"description":"Comma-separated. `schemas` includes the full JSON Schema on each advertised tool.","schema":{"type":"string"}}]},"patch":{"operationId":"updateToolbox","summary":"Update a toolbox","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Toolbox id — a stored row (`tbx_…`), or a dynamic id (`global:{user_id}` / `connection:{connection_id}`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`). For a dynamic row, the caller."},"type":{"type":"string","enum":["stored","global","connection"],"description":"`stored` = a real ACL-backed row. `global`/`connection` = computed per request, always `readonly: true`."},"readonly":{"type":"boolean","description":"True for the dynamic global/per-connection toolboxes — never editable, shareable, transferable or deletable."},"template_id":{"type":["string","null"],"description":"Provenance only (stamped at creation, never a live link) — null for a from-scratch toolbox and for every dynamic row. May dangle after the template is deleted."},"connection_id":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"connector_slug":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Always `owner` for a dynamic row. Never `oversight` — toolboxes have no org-wide oversight fallback."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Cheap per-page SQL aggregate over entries with `connection_id IS NULL` — drives the console's Status column (\"Ready\" / \"N need connection\"). Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, bounded — the console renders it as a stack of connector logos. Present on every `GET /toolbox` row: a stored row summarises its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarises the connections it spans. Present on every command response too (`POST /toolbox`, `GET /toolbox/{id}`, `PATCH /toolbox/{id}`, `POST /toolbox/{id}/transfer`), so a client that merges one into its list does not show a just-created or just-edited toolbox as having no integrations. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the toolbox."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":["string","null"],"format":"date-time","description":"Null for dynamic rows, which are computed, never stored."},"updated_at":{"type":["string","null"],"format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /toolbox` stored row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Absent from dynamic rows, and deliberately absent for a `view`/`use` grantee: the grantee set, counts included, is information about colleagues (docs/access-model.md §8). Read `can_see_shares` to distinguish \"not permitted\" from \"not carried\"; never infer it from this field's absence.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"A bounded preview of the ACL (same cap as `access_summary`), present only on `GET /toolbox/{id}` when the caller may see the ACL. Never on a list row (`GET /toolbox`) — page `GET /toolbox/{id}/share` for the full, cursor-paginated grant list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a bounded rollup — present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see the ACL (the same gate `access_summary` uses). Never the full set: page `GET /toolbox/{id}/connected-app` for that.","properties":{"count":{"type":"integer"},"preview":{"type":"array","description":"At most 5 apps, for a hover preview.","items":{"type":"object","properties":{"grant_id":{"type":"string","description":"OAuth grant id (`ogrt_…`) — what `DELETE /oauth/grant/{id}` takes, scoped to the grant's own user."},"client_id":{"type":"string"},"client_name":{"type":["string","null"],"description":"Null when the OAuth client row is gone."},"user":{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":"string","description":"Omitted along with `email` when the id no longer resolves to an org member."},"email":{"type":"string","format":"email"}}},"via":{"type":"string","enum":["toolbox","all_tools"],"description":"`toolbox`: this toolbox is named explicitly on the grant. `all_tools`: an \"All my tools\" authorization by a user who can currently use this toolbox — computed live, not a stored fact."}}}}}},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this toolbox — owner, `edit` access, or `toolbox:share`. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this toolbox's own settings — owner, or `edit` access. No `toolbox:manage` fallback. Mirrors `PATCH /toolbox/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this toolbox — ownership, full stop. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/transfer` and `DELETE /toolbox/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /toolbox/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this toolbox — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the toolbox reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this toolbox through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Entry id (`tbxe_…`) — stable across updates only if you send it back unchanged."},"type":{"type":"string","enum":["proxy","synthetic"]},"synthetic_tool_id":{"type":["string","null"],"description":"Set for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug for `proxy` entries; null for synthetic."},"connection_id":{"type":["string","null"],"description":"`null` = \"needs connection\" — stamped-but-unfilled, or a pin that broke. Never set for synthetic entries."},"delegated_by_user_id":{"type":["string","null"],"description":"Server-stamped, never a client input: whose `use` grant on `connection_id` this pin rides on — the live authority chain (docs/access-model.md §6). If this user loses `use` on the connection, the entry goes unmet for every executor, not just them. Null iff `connection_id` is null."},"tool_name":{"type":["string","null"],"description":"Connector tool name for `proxy` entries."},"overrides":{"type":"object","description":"Presentation overrides — see the entry input schema for the same shape, request-side.","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"defaults":{"type":"object","additionalProperties":true}}},"frozen_params":{"type":"object","additionalProperties":true},"enabled":{"type":"boolean"},"connection_status":{"type":["string","null"],"enum":["pending","active","needs_reauth","disconnected",null],"description":"Detail responses only: the pinned connection's live status, when resolved."},"unmet":{"type":"boolean","description":"Detail responses only: no usable connection backs this entry — pin is empty, broken, or the delegator lost `use`."},"tool_available":{"type":"boolean","description":"Detail responses only: false when the catalog (or the synthetic-tool store) no longer has this tool."},"delegation_ok":{"type":"boolean","description":"Detail responses only: false when `connection_id` is set but `delegated_by_user_id` no longer holds `use` on it. Always true when `connection_id` is null."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"Detail responses only: HOW `delegated_by_user_id` holds `connection_id` — `accessVia` over the DELEGATOR, same owner/org/team/direct precedence (broadest wins) as the caller's own `access_via` (docs/access-model.md §8). `delegation_ok` says the delegation still stands; this says what it stands on, so copy can avoid implying a personal grant where an org-wide one is doing the work. Null for a synthetic entry (its steps, not the row, hold connections), for a pin with no connection or no stamped delegator, and whenever the delegation no longer works (`delegation_ok: false`) — the delegator is no longer an active member, or their reach has fallen below `use`."},"can_repin":{"type":"boolean","description":"Detail responses only, and the only field here that differs by READER: may the caller repair this entry with `POST /toolbox/{id}/repin`? True when the entry is broken (`delegation_ok: false` on a proxy entry that pins a connection), the caller holds `use` on that connection, and the caller holds `edit` (or ownership) on this toolbox. `delegation_ok: false` is the same answer for everybody who opens the toolbox; this says who among them can do something about it. Do not re-derive it — the inputs (the caller's grants and team memberships) are not on the wire."}}},"description":"Resolved entries — always present, at every access level."},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true,"description":"Present only with `?include=schemas` — omitted by default; a toolbox spanning many connections can carry thousands of complete JSON Schemas."},"type":{"type":"string","enum":["proxy","synthetic"]},"connector_slug":{"type":["string","null"],"description":"`\"synthetic\"` sentinel is never used here; null for synthetic tools."},"connections":{"type":"array","description":"Every account this tool can reach, labelled exactly as the `connection` argument enum in `input_schema` accepts them.","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"}}}},"connection_id":{"type":["string","null"],"description":"Set only when exactly one connection backs this tool (read `connections` otherwise). Null for synthetic tools."},"synthetic_tool_id":{"type":["string","null"]}}},"description":"The resolved, runnable tool surface. Present ONLY when the caller's access reaches `use` — a view-only grantee must not see delegated connections' labels."},"delegation":{"type":["object","null"],"description":"What this toolbox RUNS ON — the disclosure side of §6 delegation. Sharing a toolbox at `use` hands the grantee execution over every connection its entries reach (pinned directly, or reached through a synthetic entry's steps, where the synthetic tool's owner is the delegator), while leaving the connections themselves invisible to them. A bounded rollup, never the set: a toolbox pinning a thousand entries across hundreds of connections returns the same size response. Present on every stored-toolbox response — detail, create, update and share — and always empty for a dynamic row, which delegates nothing. On a create/update/share response it may instead be `null`: the write succeeded and the summary alone could not be computed (never \"nothing is delegated\", which is an all-zeroes summary) — re-read `GET /toolbox/{id}` for the disclosure. Unlike `tools`, this is NOT gated on `use`: its `visible` half already withholds every name the caller has no right to, and its two totals say only how many connections and how many people stand behind the toolbox — which a grantee can already count off `entries[].connection_id`.","properties":{"connection_count":{"type":"integer","description":"Distinct connections the tools run through, whether or not the caller can see them."},"delegator_count":{"type":"integer","description":"Distinct people whose access those pins ride on. Never named at this level."},"visible":{"type":"object","description":"The same delegation restricted to what THIS caller may already see (§4). Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection `GET /connection/{id}` would 404 for them.","properties":{"connection_count":{"type":"integer"},"private_connection_count":{"type":"integer","description":"Of the visible ones, how many carry no grants at all — only their owner can reach them today."},"preview":{"type":"array","description":"At most 3 of the visible connections, private ones first.","items":{"type":"object","properties":{"connection_id":{"type":"string"},"connector":{"type":"string"},"label":{"type":"string"},"via":{"type":"string","enum":["entry","synthetic_tool"],"description":"`entry` = an entry pins it directly. `synthetic_tool` = reached only through a synthetic tool's steps."},"delegated_by":{"type":"object","description":"Whose access this delegation rides on. `name`/`email` are absent for a user this org can no longer resolve. Read it together with `delegated_by_source`.","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}}},"delegated_by_source":{"type":"string","enum":["delegator","connection_owner"],"description":"`delegator` = the pin's own stamped delegator (or, for `via: synthetic_tool`, the tool's owner): this person really is who the execution rides on. `connection_owner` = the pin carries no delegator (an entry written before the column existed, never backfilled), so `delegated_by` is the connection's OWNER standing in as the delegator of record. Such a pin is still a real delegation and is still counted; copy that names a person must branch on this rather than claim they shared it."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"HOW `delegated_by` was entitled to this connection — `accessVia` over the DELEGATOR, with §8's own precedence: the BROADEST true source wins (owner, else org, else team, else direct) and the access level plays no part. `delegated_by` alone only says who pinned it; without this, copy reads an org-wide or team grant as a personal favour and hides how far the account actually reaches. Null whenever the delegation no longer works — the delegator is no longer an active member, or their reach over the connection has fallen below `use` — since the only honest statement left is who pinned it. Same predicate as the entry's own field (`delegationStanding`), so the two surfaces cannot disagree."},"delegated_by_access_via_team":{"type":"object","description":"The team behind `delegated_by_access_via: 'team'`, present ONLY when the CALLER is themselves a member of it — §8's bound verbatim, since a third party's team is a grantee of a connection the caller may not be able to open, and the grantee list stays behind `can_see_shares`. Absent otherwise; the copy then says \"a team\" without naming it.","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"private":{"type":"boolean"},"can_share":{"type":"boolean","description":"May the caller grant someone access to this connection directly — the same derivation `GET /connection/{id}`'s `can_share` and `POST /connection/{id}/share`'s own gate use."}}}}}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"**`entries` REPLACES the entire entry list when present** — omitted entries are deleted, and the ones you send back define the toolbox in full. An incoming entry whose (connection_id, tool_name) pair matches a pre-existing row keeps that row's original delegator (the carry-forward rule, §6) rather than re-stamping whoever clicked Save; only genuinely new pairs are checked against the ACTING EDITOR's own usability and stamped with them. Omitting `entries` altogether leaves them untouched; `name`/`description` are ordinary merge-patch fields. Returns the same resolved shape `GET /toolbox/{id}` does. Rejects a dynamic id with 400 — there is no row to update. Requires `edit` access or ownership (own grant) — no `toolbox:manage` fallback.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":4000},"entries":{"type":"array","maxItems":200,"items":{"type":"object","description":"One tool in the toolbox. A `proxy` entry names a connector tool (`connector_slug` + `tool_name`), optionally pinned to a `connection_id`; a `synthetic` entry names a `synthetic_tool_id` instead.","properties":{"type":{"type":"string","enum":["proxy","synthetic"],"description":"Defaults to `proxy`."},"synthetic_tool_id":{"type":["string","null"],"description":"Required for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":["string","null"],"maxLength":200,"description":"Exact tool name from `GET /connector/{slug}/tools`."},"overrides":{"type":"object","description":"How the tool is presented to the model. Each key REPLACES the derived value.","properties":{"name":{"type":"string","minLength":1,"maxLength":64},"description":{"type":"string","maxLength":4000},"input_schema":{"type":"object","additionalProperties":true,"description":"Must be a JSON Schema object with `\"type\": \"object\"`. Replaces the derived schema entirely."},"defaults":{"type":"object","additionalProperties":true,"description":"Default argument values the caller may still override."}}},"frozen_params":{"type":"object","additionalProperties":true,"description":"Arguments pinned by the author. Unlike `overrides.defaults`, a caller cannot change these."},"enabled":{"type":"boolean"},"connection_id":{"type":["string","null"],"description":"Pin this entry to one connection (`conn_…`) the ACTING EDITOR can use — they become its delegator (docs/access-model.md §6). `null` = \"needs connection.\" On `PATCH /toolbox/{id}`, an incoming entry whose (connection_id, tool_name) pair matches a pre-existing row keeps that row's original delegator rather than re-stamping the caller (the carry-forward rule) — only genuinely new pairs get the usability check and the caller's own stamp."}}},"description":"Replaces every entry."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."}]},"delete":{"operationId":"deleteToolbox","summary":"Delete a toolbox","tags":["Toolboxes"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible: the definition, its entries and every share are gone. Connections, credentials and synthetic tools are untouched — this deletes the bundle and its pins, not the accounts or tools it pointed at. Rejects a dynamic id with 400. Requires ownership — no `toolbox:manage` fallback.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."}]}},"/toolbox/{id}/repin":{"post":{"operationId":"repinToolboxEntries","summary":"Re-pin broken entries onto your own access","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Toolbox id — a stored row (`tbx_…`), or a dynamic id (`global:{user_id}` / `connection:{connection_id}`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`). For a dynamic row, the caller."},"type":{"type":"string","enum":["stored","global","connection"],"description":"`stored` = a real ACL-backed row. `global`/`connection` = computed per request, always `readonly: true`."},"readonly":{"type":"boolean","description":"True for the dynamic global/per-connection toolboxes — never editable, shareable, transferable or deletable."},"template_id":{"type":["string","null"],"description":"Provenance only (stamped at creation, never a live link) — null for a from-scratch toolbox and for every dynamic row. May dangle after the template is deleted."},"connection_id":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"connector_slug":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Always `owner` for a dynamic row. Never `oversight` — toolboxes have no org-wide oversight fallback."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Cheap per-page SQL aggregate over entries with `connection_id IS NULL` — drives the console's Status column (\"Ready\" / \"N need connection\"). Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, bounded — the console renders it as a stack of connector logos. Present on every `GET /toolbox` row: a stored row summarises its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarises the connections it spans. Present on every command response too (`POST /toolbox`, `GET /toolbox/{id}`, `PATCH /toolbox/{id}`, `POST /toolbox/{id}/transfer`), so a client that merges one into its list does not show a just-created or just-edited toolbox as having no integrations. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the toolbox."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":["string","null"],"format":"date-time","description":"Null for dynamic rows, which are computed, never stored."},"updated_at":{"type":["string","null"],"format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /toolbox` stored row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Absent from dynamic rows, and deliberately absent for a `view`/`use` grantee: the grantee set, counts included, is information about colleagues (docs/access-model.md §8). Read `can_see_shares` to distinguish \"not permitted\" from \"not carried\"; never infer it from this field's absence.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"A bounded preview of the ACL (same cap as `access_summary`), present only on `GET /toolbox/{id}` when the caller may see the ACL. Never on a list row (`GET /toolbox`) — page `GET /toolbox/{id}/share` for the full, cursor-paginated grant list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a bounded rollup — present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see the ACL (the same gate `access_summary` uses). Never the full set: page `GET /toolbox/{id}/connected-app` for that.","properties":{"count":{"type":"integer"},"preview":{"type":"array","description":"At most 5 apps, for a hover preview.","items":{"type":"object","properties":{"grant_id":{"type":"string","description":"OAuth grant id (`ogrt_…`) — what `DELETE /oauth/grant/{id}` takes, scoped to the grant's own user."},"client_id":{"type":"string"},"client_name":{"type":["string","null"],"description":"Null when the OAuth client row is gone."},"user":{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":"string","description":"Omitted along with `email` when the id no longer resolves to an org member."},"email":{"type":"string","format":"email"}}},"via":{"type":"string","enum":["toolbox","all_tools"],"description":"`toolbox`: this toolbox is named explicitly on the grant. `all_tools`: an \"All my tools\" authorization by a user who can currently use this toolbox — computed live, not a stored fact."}}}}}},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this toolbox — owner, `edit` access, or `toolbox:share`. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this toolbox's own settings — owner, or `edit` access. No `toolbox:manage` fallback. Mirrors `PATCH /toolbox/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this toolbox — ownership, full stop. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/transfer` and `DELETE /toolbox/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /toolbox/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this toolbox — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the toolbox reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this toolbox through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}},{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Entry id (`tbxe_…`) — stable across updates only if you send it back unchanged."},"type":{"type":"string","enum":["proxy","synthetic"]},"synthetic_tool_id":{"type":["string","null"],"description":"Set for `synthetic` entries (`syn_…`)."},"connector_slug":{"type":["string","null"],"description":"Connector slug for `proxy` entries; null for synthetic."},"connection_id":{"type":["string","null"],"description":"`null` = \"needs connection\" — stamped-but-unfilled, or a pin that broke. Never set for synthetic entries."},"delegated_by_user_id":{"type":["string","null"],"description":"Server-stamped, never a client input: whose `use` grant on `connection_id` this pin rides on — the live authority chain (docs/access-model.md §6). If this user loses `use` on the connection, the entry goes unmet for every executor, not just them. Null iff `connection_id` is null."},"tool_name":{"type":["string","null"],"description":"Connector tool name for `proxy` entries."},"overrides":{"type":"object","description":"Presentation overrides — see the entry input schema for the same shape, request-side.","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"defaults":{"type":"object","additionalProperties":true}}},"frozen_params":{"type":"object","additionalProperties":true},"enabled":{"type":"boolean"},"connection_status":{"type":["string","null"],"enum":["pending","active","needs_reauth","disconnected",null],"description":"Detail responses only: the pinned connection's live status, when resolved."},"unmet":{"type":"boolean","description":"Detail responses only: no usable connection backs this entry — pin is empty, broken, or the delegator lost `use`."},"tool_available":{"type":"boolean","description":"Detail responses only: false when the catalog (or the synthetic-tool store) no longer has this tool."},"delegation_ok":{"type":"boolean","description":"Detail responses only: false when `connection_id` is set but `delegated_by_user_id` no longer holds `use` on it. Always true when `connection_id` is null."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"Detail responses only: HOW `delegated_by_user_id` holds `connection_id` — `accessVia` over the DELEGATOR, same owner/org/team/direct precedence (broadest wins) as the caller's own `access_via` (docs/access-model.md §8). `delegation_ok` says the delegation still stands; this says what it stands on, so copy can avoid implying a personal grant where an org-wide one is doing the work. Null for a synthetic entry (its steps, not the row, hold connections), for a pin with no connection or no stamped delegator, and whenever the delegation no longer works (`delegation_ok: false`) — the delegator is no longer an active member, or their reach has fallen below `use`."},"can_repin":{"type":"boolean","description":"Detail responses only, and the only field here that differs by READER: may the caller repair this entry with `POST /toolbox/{id}/repin`? True when the entry is broken (`delegation_ok: false` on a proxy entry that pins a connection), the caller holds `use` on that connection, and the caller holds `edit` (or ownership) on this toolbox. `delegation_ok: false` is the same answer for everybody who opens the toolbox; this says who among them can do something about it. Do not re-derive it — the inputs (the caller's grants and team memberships) are not on the wire."}}},"description":"Resolved entries — always present, at every access level."},"tools":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true,"description":"Present only with `?include=schemas` — omitted by default; a toolbox spanning many connections can carry thousands of complete JSON Schemas."},"type":{"type":"string","enum":["proxy","synthetic"]},"connector_slug":{"type":["string","null"],"description":"`\"synthetic\"` sentinel is never used here; null for synthetic tools."},"connections":{"type":"array","description":"Every account this tool can reach, labelled exactly as the `connection` argument enum in `input_schema` accepts them.","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"}}}},"connection_id":{"type":["string","null"],"description":"Set only when exactly one connection backs this tool (read `connections` otherwise). Null for synthetic tools."},"synthetic_tool_id":{"type":["string","null"]}}},"description":"The resolved, runnable tool surface. Present ONLY when the caller's access reaches `use` — a view-only grantee must not see delegated connections' labels."},"delegation":{"type":["object","null"],"description":"What this toolbox RUNS ON — the disclosure side of §6 delegation. Sharing a toolbox at `use` hands the grantee execution over every connection its entries reach (pinned directly, or reached through a synthetic entry's steps, where the synthetic tool's owner is the delegator), while leaving the connections themselves invisible to them. A bounded rollup, never the set: a toolbox pinning a thousand entries across hundreds of connections returns the same size response. Present on every stored-toolbox response — detail, create, update and share — and always empty for a dynamic row, which delegates nothing. On a create/update/share response it may instead be `null`: the write succeeded and the summary alone could not be computed (never \"nothing is delegated\", which is an all-zeroes summary) — re-read `GET /toolbox/{id}` for the disclosure. Unlike `tools`, this is NOT gated on `use`: its `visible` half already withholds every name the caller has no right to, and its two totals say only how many connections and how many people stand behind the toolbox — which a grantee can already count off `entries[].connection_id`.","properties":{"connection_count":{"type":"integer","description":"Distinct connections the tools run through, whether or not the caller can see them."},"delegator_count":{"type":"integer","description":"Distinct people whose access those pins ride on. Never named at this level."},"visible":{"type":"object","description":"The same delegation restricted to what THIS caller may already see (§4). Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection `GET /connection/{id}` would 404 for them.","properties":{"connection_count":{"type":"integer"},"private_connection_count":{"type":"integer","description":"Of the visible ones, how many carry no grants at all — only their owner can reach them today."},"preview":{"type":"array","description":"At most 3 of the visible connections, private ones first.","items":{"type":"object","properties":{"connection_id":{"type":"string"},"connector":{"type":"string"},"label":{"type":"string"},"via":{"type":"string","enum":["entry","synthetic_tool"],"description":"`entry` = an entry pins it directly. `synthetic_tool` = reached only through a synthetic tool's steps."},"delegated_by":{"type":"object","description":"Whose access this delegation rides on. `name`/`email` are absent for a user this org can no longer resolve. Read it together with `delegated_by_source`.","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}}},"delegated_by_source":{"type":"string","enum":["delegator","connection_owner"],"description":"`delegator` = the pin's own stamped delegator (or, for `via: synthetic_tool`, the tool's owner): this person really is who the execution rides on. `connection_owner` = the pin carries no delegator (an entry written before the column existed, never backfilled), so `delegated_by` is the connection's OWNER standing in as the delegator of record. Such a pin is still a real delegation and is still counted; copy that names a person must branch on this rather than claim they shared it."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"HOW `delegated_by` was entitled to this connection — `accessVia` over the DELEGATOR, with §8's own precedence: the BROADEST true source wins (owner, else org, else team, else direct) and the access level plays no part. `delegated_by` alone only says who pinned it; without this, copy reads an org-wide or team grant as a personal favour and hides how far the account actually reaches. Null whenever the delegation no longer works — the delegator is no longer an active member, or their reach over the connection has fallen below `use` — since the only honest statement left is who pinned it. Same predicate as the entry's own field (`delegationStanding`), so the two surfaces cannot disagree."},"delegated_by_access_via_team":{"type":"object","description":"The team behind `delegated_by_access_via: 'team'`, present ONLY when the CALLER is themselves a member of it — §8's bound verbatim, since a third party's team is a grantee of a connection the caller may not be able to open, and the grantee list stays behind `can_see_shares`. Absent otherwise; the copy then says \"a team\" without naming it.","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"private":{"type":"boolean"},"can_share":{"type":"boolean","description":"May the caller grant someone access to this connection directly — the same derivation `GET /connection/{id}`'s `can_share` and `POST /connection/{id}/share`'s own gate use."}}}}}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Repairs every entry of this toolbox that pins `connection_id` and whose delegation has lapsed — the `delegation_ok: false` state — by stamping `delegated_by_user_id` to YOU. Keyed on the connection, not on one entry, because a lapsed grant breaks every entry that pins that account at once.\n\nIt is never automatic and there is no \"on behalf of\" parameter: re-pinning makes you the authorizer of record for everyone the toolbox is shared with at `use`, so the call itself is your consent (docs/access-model.md §6). Requires `edit` (or ownership) on the toolbox AND `use` on the connection — the same gate every other pin passes. A healthy entry is left alone, so this can never quietly take a working delegation over from a colleague; `repinned_count: 0` with a 200 means there was nothing to repair.\n\nAnswers the same body `PATCH /toolbox/{id}` does (resolved entries, capabilities, delegation summary) plus `repinned_count`, so a client can merge it straight over its own row. Rejects a dynamic id with 400.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["connection_id"],"properties":{"connection_id":{"type":"string","description":"The pinned connection whose broken entries you are taking over (`conn_…`)."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."}]}},"/toolbox/{id}/transfer":{"post":{"operationId":"transferToolbox","summary":"Transfer toolbox ownership","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Toolbox id — a stored row (`tbx_…`), or a dynamic id (`global:{user_id}` / `connection:{connection_id}`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"owner_user_id":{"type":"string","description":"Creator (`usr_…`). For a dynamic row, the caller."},"type":{"type":"string","enum":["stored","global","connection"],"description":"`stored` = a real ACL-backed row. `global`/`connection` = computed per request, always `readonly: true`."},"readonly":{"type":"boolean","description":"True for the dynamic global/per-connection toolboxes — never editable, shareable, transferable or deletable."},"template_id":{"type":["string","null"],"description":"Provenance only (stamped at creation, never a live link) — null for a from-scratch toolbox and for every dynamic row. May dangle after the template is deleted."},"connection_id":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"connector_slug":{"type":"string","description":"Present only for `type: \"connection\"` dynamic rows."},"access_level":{"type":"string","description":"The caller's access: `owner`, or a granted `view`/`use`/`edit` level. Always `owner` for a dynamic row. Never `oversight` — toolboxes have no org-wide oversight fallback."},"owner":{"type":["object","null"],"description":"Owner summary — resolved for a row the caller does not themselves own (a toolbox shared with them), so the UI always knows whose row it is looking at. Absent for a toolbox the caller owns.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Cheap per-page SQL aggregate over entries with `connection_id IS NULL` — drives the console's Status column (\"Ready\" / \"N need connection\"). Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, bounded — the console renders it as a stack of connector logos. Present on every `GET /toolbox` row: a stored row summarises its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarises the connections it spans. Present on every command response too (`POST /toolbox`, `GET /toolbox/{id}`, `PATCH /toolbox/{id}`, `POST /toolbox/{id}/transfer`), so a client that merges one into its list does not show a just-created or just-edited toolbox as having no integrations. `total: 0` means no connector-backed tools (empty, or synthetic-only), never \"not computed\".","properties":{"total":{"type":"integer","description":"Distinct connector slugs across the toolbox."},"preview":{"type":"array","description":"At most 5 connectors, ordered by entry count descending then slug ascending — the dominant integration leads and the order is stable across requests. Each entry arrives resolved: there is no follow-up `GET /connector/{slug}` to make, and a page of rows costs no per-row catalog lookup.","items":{"type":"object","properties":{"slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"name":{"type":"string","description":"Catalog label. Falls back to the slug when the connector no longer resolves (deleted from the catalog), so a row always has something to draw."},"logo":{"type":["string","null"],"description":"The connector's square `icon` when it has one, else its wordmark `logo`, else null (the connector carries neither picture, or could not be resolved). Draw it in a square tile; render initials from `name` when it is null."}}}}}},"created_at":{"type":["string","null"],"format":"date-time","description":"Null for dynamic rows, which are computed, never stored."},"updated_at":{"type":["string","null"],"format":"date-time"},"access_summary":{"type":"object","description":"Present on a `GET /toolbox` stored row exactly when that row's `can_see_shares` is `true` — the caller owns it, holds `edit`, or administers a team it is granted to. Absent from dynamic rows, and deliberately absent for a `view`/`use` grantee: the grantee set, counts included, is information about colleagues (docs/access-model.md §8). Read `can_see_shares` to distinguish \"not permitted\" from \"not carried\"; never infer it from this field's absence.","properties":{"org_level":{"type":["string","null"],"enum":["view","use","edit",null],"description":"Level of the org-wide grant, or null when there is none."},"team_count":{"type":"integer"},"user_count":{"type":"integer"},"total":{"type":"integer","description":"Every grant, the org-wide one included."},"preview":{"type":"array","description":"At most 5 grantees, broadest first (org, then teams, then members), for a hover preview.","items":{"type":"object","properties":{"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"]},"level":{"type":"string","enum":["view","use","edit"]},"name":{"type":["string","null"],"description":"Display name; null for org grants and for grantees no longer in the org."}}}}}},"shares":{"type":"array","description":"A bounded preview of the ACL (same cap as `access_summary`), present only on `GET /toolbox/{id}` when the caller may see the ACL. Never on a list row (`GET /toolbox`) — page `GET /toolbox/{id}/share` for the full, cursor-paginated grant list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a bounded rollup — present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see the ACL (the same gate `access_summary` uses). Never the full set: page `GET /toolbox/{id}/connected-app` for that.","properties":{"count":{"type":"integer"},"preview":{"type":"array","description":"At most 5 apps, for a hover preview.","items":{"type":"object","properties":{"grant_id":{"type":"string","description":"OAuth grant id (`ogrt_…`) — what `DELETE /oauth/grant/{id}` takes, scoped to the grant's own user."},"client_id":{"type":"string"},"client_name":{"type":["string","null"],"description":"Null when the OAuth client row is gone."},"user":{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":"string","description":"Omitted along with `email` when the id no longer resolves to an org member."},"email":{"type":"string","format":"email"}}},"via":{"type":"string","enum":["toolbox","all_tools"],"description":"`toolbox`: this toolbox is named explicitly on the grant. `all_tools`: an \"All my tools\" authorization by a user who can currently use this toolbox — computed live, not a stored fact."}}}}}},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — the caller's grant-or-ownership level (§10). Present on every list and detail row for both templates and toolboxes — the two ACL-backed resource types with a `use`-gated action of their own."},"can_share":{"type":"boolean","description":"Whether the caller may share this toolbox — owner, `edit` access, or `toolbox:share`. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/share`'s own check."},"can_manage":{"type":"boolean","description":"Whether the caller may edit this toolbox's own settings — owner, or `edit` access. No `toolbox:manage` fallback. Mirrors `PATCH /toolbox/{id}`."},"can_transfer":{"type":"boolean","description":"Whether the caller may transfer or delete this toolbox — ownership, full stop. No `toolbox:manage` fallback. Mirrors `POST /toolbox/{id}/transfer` and `DELETE /toolbox/{id}`."},"can_revoke_share":{"type":"boolean","description":"`can_share`, verbatim — a THIRD formula, distinct from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but was never meant to grant or revoke someone else's access to it. Mirrors `DELETE /toolbox/{id}/share/{aclId}`."},"access_via":{"type":"string","enum":["owner","direct","team","org"],"description":"How the CALLER reaches this toolbox — not who else can. `owner` — they own it. `direct` — a grant naming them personally. `team` — a grant to a team they belong to (or, per §6.2, one they administer), named in `access_via_team`. `org` — an organization-wide grant. When several sources apply the BROADEST wins and the caller's access level is not consulted: owner, else `org`, else `team`, else `direct`. So a caller granted `edit` personally AND `view` org-wide reads `org` — a narrower grant must never mask org-wide exposure, since this field says how far the toolbox reaches, not what the caller may do with it. Deliberately NOT gated on `can_see_shares`, and deliberately not a widening of it: this is the caller's OWN grant and their OWN team memberships, so a `view`/`use` grantee receives it while the grantee list — information about colleagues — stays closed to them. No other member is ever named. ABSENT when nothing reaches the caller — a transfer answering the ex-owner of a resource that was never shared, or a catalog row browsed with no grant behind it. Absent means \"no source to name\", never \"not permitted\", and never an implied `org`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this toolbox through — one of their own teams, never a disclosure about anybody else.","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":["string","null"],"description":"Team display name, or null when the team no longer resolves in the directory — the same null contract every resolved grantee name carries."}}}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Changes `owner_user_id` — and nothing else. Every existing grant is left exactly as it was, so nobody who could already reach it loses that reach; the new owner gains only what ownership itself confers (implicit `edit`, plus delete and transfer). Each entry's own delegator (whoever held `use` when they pinned it) is untouched by an ownership transfer of the TOOLBOX — that is a separate fact from who owns the pinned connection. Rejects a dynamic id with 400. Allowed for the current owner only — no `toolbox:manage` fallback.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner_user_id"],"properties":{"owner_user_id":{"type":"string","description":"New owning member (`usr_…`)."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."}]}},"/toolbox/{id}/share":{"get":{"operationId":"listToolboxShares","summary":"List toolbox ACL shares","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"ACL id (`acl_…`) — what `DELETE …/share/{aclId}` takes."},"grantee_type":{"type":"string","enum":["user","team","org"]},"grantee_id":{"type":["string","null"],"description":"Null for org-wide grants."},"level":{"type":"string","enum":["view","use","edit"]},"grantee":{"type":"object","description":"Resolved display data, so a page of grants needs no follow-up lookups.","properties":{"name":{"type":["string","null"]},"email":{"type":["string","null"]},"member_count":{"type":["integer","null"],"description":"Team grants only: how many people the grant reaches."}}},"created_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Cursor-paginated, and the only place the grant list is served — a toolbox may be granted to every member of the organization, so nothing else embeds it. Filter with `grantee_type`, search member names/emails and team names with `q`. Requires owner or `edit` access (own grant) — no `toolbox:manage` fallback.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Match member name or email, or team name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"grantee_type","in":"query","required":false,"description":"Restrict to one kind of grantee.","schema":{"type":"string","enum":["user","team","org"]}}]},"post":{"operationId":"shareToolbox","summary":"Share a toolbox","tags":["Toolboxes"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AclEntry"},{"type":"object","properties":{"delegation":{"type":["object","null"],"description":"What this toolbox RUNS ON — the disclosure side of §6 delegation. Sharing a toolbox at `use` hands the grantee execution over every connection its entries reach (pinned directly, or reached through a synthetic entry's steps, where the synthetic tool's owner is the delegator), while leaving the connections themselves invisible to them. A bounded rollup, never the set: a toolbox pinning a thousand entries across hundreds of connections returns the same size response. Present on every stored-toolbox response — detail, create, update and share — and always empty for a dynamic row, which delegates nothing. On a create/update/share response it may instead be `null`: the write succeeded and the summary alone could not be computed (never \"nothing is delegated\", which is an all-zeroes summary) — re-read `GET /toolbox/{id}` for the disclosure.","properties":{"connection_count":{"type":"integer","description":"Distinct connections the tools run through, whether or not the caller can see them."},"delegator_count":{"type":"integer","description":"Distinct people whose access those pins ride on. Never named at this level."},"visible":{"type":"object","description":"The same delegation restricted to what THIS caller may already see (§4). Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection `GET /connection/{id}` would 404 for them.","properties":{"connection_count":{"type":"integer"},"private_connection_count":{"type":"integer","description":"Of the visible ones, how many carry no grants at all — only their owner can reach them today."},"preview":{"type":"array","description":"At most 3 of the visible connections, private ones first.","items":{"type":"object","properties":{"connection_id":{"type":"string"},"connector":{"type":"string"},"label":{"type":"string"},"via":{"type":"string","enum":["entry","synthetic_tool"],"description":"`entry` = an entry pins it directly. `synthetic_tool` = reached only through a synthetic tool's steps."},"delegated_by":{"type":"object","description":"Whose access this delegation rides on. `name`/`email` are absent for a user this org can no longer resolve. Read it together with `delegated_by_source`.","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}}},"delegated_by_source":{"type":"string","enum":["delegator","connection_owner"],"description":"`delegator` = the pin's own stamped delegator (or, for `via: synthetic_tool`, the tool's owner): this person really is who the execution rides on. `connection_owner` = the pin carries no delegator (an entry written before the column existed, never backfilled), so `delegated_by` is the connection's OWNER standing in as the delegator of record. Such a pin is still a real delegation and is still counted; copy that names a person must branch on this rather than claim they shared it."},"delegated_by_access_via":{"type":["string","null"],"enum":["owner","direct","team","org",null],"description":"HOW `delegated_by` was entitled to this connection — `accessVia` over the DELEGATOR, with §8's own precedence: the BROADEST true source wins (owner, else org, else team, else direct) and the access level plays no part. `delegated_by` alone only says who pinned it; without this, copy reads an org-wide or team grant as a personal favour and hides how far the account actually reaches. Null whenever the delegation no longer works — the delegator is no longer an active member, or their reach over the connection has fallen below `use` — since the only honest statement left is who pinned it. Same predicate as the entry's own field (`delegationStanding`), so the two surfaces cannot disagree."},"delegated_by_access_via_team":{"type":"object","description":"The team behind `delegated_by_access_via: 'team'`, present ONLY when the CALLER is themselves a member of it — §8's bound verbatim, since a third party's team is a grantee of a connection the caller may not be able to open, and the grantee list stays behind `can_see_shares`. Absent otherwise; the copy then says \"a team\" without naming it.","properties":{"id":{"type":"string"},"name":{"type":"string"}}},"private":{"type":"boolean"},"can_share":{"type":"boolean","description":"May the caller grant someone access to this connection directly — the same derivation `GET /connection/{id}`'s `can_share` and `POST /connection/{id}/share`'s own gate use."}}}}}}}}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Grants one user, team, or the whole org access at a level. Levels are ordered `view` < `use` < `edit`: `use` DELEGATES — the grantee executes tools on this toolbox, running each pinned entry through ITS delegator's own authority, never their own. Re-granting the same grantee replaces the level rather than stacking. The response repeats the toolbox's `delegation` summary — the same bounded rollup `GET /toolbox/{id}` carries, so a client that shares without ever having read the detail still learns what it just handed out. Rejects a dynamic id with 400 — there is no ACL behind one. Requires `edit` access or ownership, and `toolbox:share`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareRequest"}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."}]}},"/toolbox/{id}/connected-app":{"get":{"operationId":"listToolboxConnectedApps","summary":"List the OAuth-connected apps that reach this toolbox","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"allOf":[{"type":"object","properties":{"grant_id":{"type":"string","description":"OAuth grant id (`ogrt_…`) — what `DELETE /oauth/grant/{id}` takes, scoped to the grant's own user."},"client_id":{"type":"string"},"client_name":{"type":["string","null"],"description":"Null when the OAuth client row is gone."},"user":{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":"string","description":"Omitted along with `email` when the id no longer resolves to an org member."},"email":{"type":"string","format":"email"}}},"via":{"type":"string","enum":["toolbox","all_tools"],"description":"`toolbox`: this toolbox is named explicitly on the grant. `all_tools`: an \"All my tools\" authorization by a user who can currently use this toolbox — computed live, not a stored fact."}}},{"type":"object","properties":{"scopes":{"type":"array","items":{"type":"string"},"description":"The grant's MCP OAuth scopes (`mcp:read`, `mcp:write`, …)."},"last_used_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"is_own":{"type":"boolean","description":"True when this is the caller's own authorization."}}}]}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Cursor-paginated. Gated EXACTLY like `GET /toolbox/{id}/share` (same loader, same audience) — an app reaching a toolbox is exactly as sensitive as who the toolbox is shared with. `via` says how the app reaches it: `toolbox` when this toolbox is named explicitly on the grant, `all_tools` when the grant authorized \"All my tools\" for a user who can currently use this toolbox — computed live against the grantee's own access, never a stored fact that could go stale. `is_own` flags the caller's own authorization. Requires owner or `edit` access (own grant) — no `toolbox:manage` fallback.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]}},"/toolbox/{id}/share/{aclId}":{"delete":{"operationId":"revokeToolboxShare","summary":"Revoke a toolbox share","tags":["Toolboxes"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The grantee loses access immediately — including every tool they reached only through this delegation. Rejects a dynamic id with 400. Requires `can_revoke_share` (`can_share`, i.e. `edit` access plus `toolbox:share`) — no `toolbox:manage` fallback.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."},{"name":"aclId","in":"path","required":true,"schema":{"type":"string"},"description":"ACL entry id from the share list."}]}},"/synthetic-tool":{"get":{"operationId":"listSyntheticTools","summary":"List synthetic tools","tags":["Synthetic tools"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","additionalProperties":true}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Synthetic tools (`syn_…`) are multi-step tools defined in Elaichi rather than derived from a connector. They are placed in a template or toolbox as an entry of `type: \"synthetic\"`. Each row carries `owner` (`{ id, name, email }`, resolved server-side one batch per page) alongside `owner_user_id`; read it rather than resolving the id against the paginated member directory.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the tool's name or description. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]},"post":{"operationId":"createSyntheticTool","summary":"Create a synthetic tool","tags":["Synthetic tools"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Each step calls a connector tool; steps may reference earlier results. `input_schema` is the JSON Schema the model sees. Add it to a template or toolbox with an entry naming its `synthetic_tool_id`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","steps"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"steps":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/synthetic-tool/{id}":{"get":{"operationId":"getSyntheticTool","summary":"Get a synthetic tool","tags":["Synthetic tools"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Synthetic tool id (`syn_…`)."}]},"patch":{"operationId":"updateSyntheticTool","summary":"Update a synthetic tool","tags":["Synthetic tools"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Merge-patch at the top level, but **`steps` and `input_schema` are replaced wholesale when present**. Every template/toolbox entry referencing this tool picks the change up immediately.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Synthetic tool id (`syn_…`)."}]},"delete":{"operationId":"deleteSyntheticTool","summary":"Delete a synthetic tool","tags":["Synthetic tools"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible. Template/toolbox entries that reference it stop resolving to a tool.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Synthetic tool id (`syn_…`)."}]}},"/synthetic-tool/{id}/execute":{"post":{"operationId":"executeSyntheticTool","summary":"Execute a synthetic tool","tags":["Synthetic tools"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Runs the tool now, against live third-party systems, using the caller's connections — this is not a dry run. Arguments are validated against the tool's `input_schema`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"arguments":{"type":"object","additionalProperties":true,"description":"Matching the tool's input schema."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Synthetic tool id (`syn_…`)."}]}},"/mcp":{"post":{"operationId":"mcpControlPlaneJsonRpc","summary":"MCP control plane (OAuth-authenticated)","tags":["MCP control plane"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{},"result":{},"error":{"type":"object","description":"JSON-RPC error object. Transport-level failures use HTTP status codes instead.","additionalProperties":true}}}}}},"202":{"description":"Accepted — a JSON-RPC notification (no `id`) produces no response body."},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid, expired or revoked access token. Carries `WWW-Authenticate` with `resource_metadata`; `error=\"invalid_token\"` is present only when a credential was actually supplied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"headers":{"WWW-Authenticate":{"description":"RFC 9728 §5.1 challenge pointing at the protected-resource metadata document.","schema":{"type":"string"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The endpoint an MCP client (Claude Desktop, a claude.ai connector, Cursor) connects to after the user signs in to Elaichi. JSON-RPC 2.0 over the MCP Streamable HTTP transport, stateless, and it exposes the whole organization for one user: the control-plane operation catalog (members, teams, roles, connections, templates, toolboxes…) as `elaichi__*` tools and — when the grant carries `mcp:tools` — the user's own connected third-party tools (every tool of every connection they can use, resolved through their virtual `global:{user_id}` toolbox) under their own `{account}__{tool}` names.\n\n**Authentication.** `Authorization: Bearer <MCP OAuth access token>` from `POST /oauth/token`. Session cookies and org API tokens are not accepted here. The organization is pinned on the grant at consent time, so `X-Organization-Id` is neither sent nor honoured.\n\n**Discovery.** A 401 carries a `WWW-Authenticate: Bearer resource_metadata=\"…\"` header pointing at `/.well-known/oauth-protected-resource`. That header is how a client that holds no credential finds the authorization server and starts the OAuth flow — a bare 401 is a dead end. A valid token that simply lacks a scope gets `error=\"insufficient_scope\"` with the required `scope`, meaning re-authorize for more rather than re-authenticate.\n\n**Authorization.** Two independent gates, both of which must pass: the grant's scopes (what the user let this client do) and the user's RBAC permissions, re-read per request so a revoked role takes effect before the token expires. `tools/list` advertises only what the scopes cover, and `tools/call` re-checks server-side. Revoking the grant kills the token on its very next request. Requires an active Gold or Black subscription; a non-member gets 403, not 401. Rate limited per token.","security":[{"mcpOAuth":["mcp:read","mcp:write","mcp:destructive","mcp:tools","openid","email"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jsonrpc","method"],"properties":{"jsonrpc":{"type":"string","enum":["2.0"]},"id":{"description":"String or number. Omitted for notifications, which answer 202 with no body."},"method":{"type":"string","description":"MCP method: `initialize`, `notifications/initialized`, `tools/list`, `tools/call`, `ping`, and the empty `resources/list` / `prompts/list`."},"params":{"type":"object","additionalProperties":true}}}}}}},"get":{"operationId":"mcpControlPlaneGetNotAllowed","summary":"Not allowed (POST-only transport)","tags":["MCP control plane"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"Method Not Allowed, with `Allow: POST`. This transport is POST-only: there is no server-initiated SSE stream to open with GET, and no session to tear down with DELETE."},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[]},"delete":{"operationId":"mcpControlPlaneDeleteNotAllowed","summary":"Not allowed (POST-only transport)","tags":["MCP control plane"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"405":{"description":"Method Not Allowed, with `Allow: POST`. This transport is POST-only: there is no server-initiated SSE stream to open with GET, and no session to tear down with DELETE."},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[]}},"/.well-known/oauth-protected-resource":{"get":{"operationId":"getOAuthProtectedResourceMetadata","summary":"Protected resource metadata (RFC 9728)","tags":["OAuth discovery"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"resource":{"type":"string","format":"uri","description":"Canonical resource identifier of the MCP endpoint — the RFC 8707 audience."},"authorization_servers":{"type":"array","items":{"type":"string","format":"uri"},"description":"Issuers permitted to authorize access to this resource."},"scopes_supported":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:destructive","mcp:tools","openid","email"]}},"bearer_methods_supported":{"type":"array","items":{"type":"string"},"description":"Always `[\"header\"]` — the token goes in `Authorization`, never in a query string."},"resource_documentation":{"type":"string","format":"uri"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Describes the MCP endpoint and names the authorization server that protects it. Public, unauthenticated, and readable cross-origin from any origin by design (a wildcard, deliberately uncredentialed CORS policy) — an MCP client fetches it before it holds any credential, often from a browser context. Discovery starts here: an unauthenticated `POST /mcp` answers 401 with a `WWW-Authenticate` header whose `resource_metadata` parameter points at this document; follow `authorization_servers[0]` to `/.well-known/oauth-authorization-server` for the endpoint URLs. Cached for one hour.","security":[]}},"/.well-known/oauth-authorization-server":{"get":{"operationId":"getOAuthAuthorizationServerMetadata","summary":"Authorization server metadata (RFC 8414)","tags":["OAuth discovery"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"issuer":{"type":"string","format":"uri"},"authorization_endpoint":{"type":"string","format":"uri"},"token_endpoint":{"type":"string","format":"uri"},"registration_endpoint":{"type":"string","format":"uri"},"revocation_endpoint":{"type":"string","format":"uri"},"response_types_supported":{"type":"array","items":{"type":"string"}},"grant_types_supported":{"type":"array","items":{"type":"string","enum":["authorization_code","refresh_token"]}},"code_challenge_methods_supported":{"type":"array","items":{"type":"string","enum":["S256"]}},"token_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string","enum":["none","client_secret_post","client_secret_basic"]}},"revocation_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string"}},"scopes_supported":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:destructive","mcp:tools","openid","email"]}},"service_documentation":{"type":"string","format":"uri"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Endpoint URLs and capabilities of the MCP authorization server. Public, unauthenticated and CORS-open on the same terms as the protected-resource document. `code_challenge_methods_supported: [\"S256\"]` is load-bearing: it is the only machine-readable statement that PKCE is mandatory and that `plain` will be rejected. Dynamic client registration (RFC 7591) is advertised; client-id metadata documents (CIMD) deliberately are not. Cached for one hour.","security":[]}},"/.well-known/openid-configuration":{"get":{"operationId":"getOpenIdConfiguration","summary":"OpenID Connect discovery document (OIDC Discovery 1.0 §3)","tags":["OAuth discovery"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"issuer":{"type":"string","format":"uri"},"authorization_endpoint":{"type":"string","format":"uri"},"token_endpoint":{"type":"string","format":"uri"},"userinfo_endpoint":{"type":"string","format":"uri"},"registration_endpoint":{"type":"string","format":"uri"},"revocation_endpoint":{"type":"string","format":"uri"},"response_types_supported":{"type":"array","items":{"type":"string"}},"grant_types_supported":{"type":"array","items":{"type":"string","enum":["authorization_code","refresh_token"]}},"subject_types_supported":{"type":"array","items":{"type":"string","enum":["public"]},"description":"Always `[\"public\"]` — the subject is the same `sub` for every client, never pairwise."},"code_challenge_methods_supported":{"type":"array","items":{"type":"string","enum":["S256"]}},"token_endpoint_auth_methods_supported":{"type":"array","items":{"type":"string","enum":["none","client_secret_post","client_secret_basic"]}},"scopes_supported":{"type":"array","items":{"type":"string","enum":["mcp:read","mcp:write","mcp:destructive","mcp:tools","openid","email"]}},"claims_supported":{"type":"array","items":{"type":"string","enum":["sub","email","email_verified"]},"description":"Exactly the claims `/oauth/userinfo` can return, and no more."},"service_documentation":{"type":"string","format":"uri"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Turns this authorization server into an OpenID provider as far as a relying party is concerned — the control an OpenAI ChatGPT Business or Enterprise admin needs to restrict a connector to their own domain. It restates `/.well-known/oauth-authorization-server` rather than configuring a second server: same `issuer`, endpoints, `openid`/`email`-inclusive scope catalog and S256-only PKCE rule, so a client reading either document reaches the same server. Public, unauthenticated, CORS-open and cached for one hour on the same terms as the other two `.well-known` documents.\n\n**Deliberately missing two members OIDC Discovery marks REQUIRED: `jwks_uri` and `id_token_signing_alg_values_supported`.** Both describe how to verify an ID token, and this server issues none — identity is read from `GET/POST /oauth/userinfo` instead, which OpenAI's own documentation names as the mandatory half (the ID token is the alternative). An absent member fails honestly; a present and false one would not. `subject_types_supported: [\"public\"]` matches what `/oauth/userinfo` actually returns: the same subject to every client, never a pairwise identifier.","security":[]}},"/oauth/register":{"post":{"operationId":"registerOAuthClient","summary":"Register an MCP client (RFC 7591)","tags":["MCP OAuth"],"responses":{"201":{"description":"Client registered","content":{"application/json":{"schema":{"type":"object","properties":{"client_id":{"type":"string","description":"OAuth client id (`ocli_…`)."},"client_secret":{"type":"string","description":"Present only for confidential clients. Returned exactly once — store it now."},"client_id_issued_at":{"type":"integer","description":"Unix seconds."},"client_secret_expires_at":{"type":"integer","description":"`0` — the secret does not expire."},"client_name":{"type":"string"},"redirect_uris":{"type":"array","items":{"type":"string"}},"grant_types":{"type":"array","items":{"type":"string"}},"response_types":{"type":"array","items":{"type":"string"}},"token_endpoint_auth_method":{"type":"string"},"client_uri":{"type":"string"},"logo_uri":{"type":"string"},"scope":{"type":"string","description":"Space-delimited list of every scope this server supports."}}}}}},"400":{"description":"RFC 6749 error body: `invalid_client_metadata` or `invalid_redirect_uri`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (`temporarily_unavailable`), with `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}},"description":"Dynamic client registration. Public and unauthenticated by protocol — any MCP client may register itself, which is why every field is length- and count-bounded and every redirect URI is scheme-checked. The response is the only time `client_secret` is ever returned; it is stored hashed and cannot be read back. Omitting `token_endpoint_auth_method` (or sending `none`) registers a public client that authenticates with PKCE alone, which is how every native MCP client works. Registration grants nothing on its own: a registered client can only act after a user approves it at `GET /oauth/authorize`. Rate limited per client IP.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["redirect_uris"],"properties":{"client_name":{"type":"string","maxLength":200,"description":"Shown on the consent screen. Defaults to \"MCP client\". Not verified — see `client_name` on the grant schema."},"redirect_uris":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string","maxLength":2048},"description":"Where authorization codes may be delivered, matched later by exact string. Accepted: any `https` URI; `http` only on loopback (`localhost`, `127.0.0.1`, `[::1]`), the RFC 8252 native-app pattern; or a private-use reverse-DNS scheme such as `com.example.app:/callback`. Fragments and wildcards are rejected."},"grant_types":{"type":"array","maxItems":8,"items":{"type":"string","enum":["authorization_code","refresh_token"]},"description":"Defaults to both. Any other value is rejected."},"response_types":{"type":"array","maxItems":8,"items":{"type":"string","enum":["code"]},"description":"Only `[\"code\"]` is supported."},"token_endpoint_auth_method":{"type":"string","enum":["none","client_secret_post","client_secret_basic"],"description":"Defaults to `none` (public client, PKCE only). Choosing either `client_secret_*` method issues a secret that the token and revocation endpoints will then require."},"client_uri":{"type":"string","maxLength":2048},"logo_uri":{"type":"string","maxLength":2048,"description":"Rendered on the consent screen beside the app name, `https:` only — an `http:`, `data:` or `blob:` value falls back to a generated monogram, as does one that fails to load. It is fetched with `referrer-policy: no-referrer` and without credentials. Note that it proves nothing: registration is unauthenticated, so this is a mark the registrant chose, exactly like `client_name`, and the consent screen says so to the user. `redirect_uris` remains the only identity signal a lookalike cannot forge."},"scope":{"type":"string","maxLength":200,"description":"Accepted and ignored; scopes are chosen per authorization."}}}}}}}},"/oauth/authorize":{"get":{"operationId":"startOAuthAuthorization","summary":"Start an authorization (browser redirect)","tags":["MCP OAuth"],"responses":{"302":{"description":"Redirect to the consent screen (or to login, then the consent screen), or back to the registered redirect URI carrying `error`/`error_description`/`state`."},"400":{"description":"Rendered error (never a redirect): unknown `client_id`, missing or unregistered `redirect_uri`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (`temporarily_unavailable`), with `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}},"description":"Validates the request, parks it server-side, and redirects the browser to Elaichi's consent screen — this endpoint never renders HTML and never returns a code directly. PKCE is mandatory: `code_challenge` with `code_challenge_method=S256`; an absent method is rejected rather than defaulting to `plain`. Nothing about the request travels through the browser afterwards — the redirect carries only an opaque request id, so there is nothing for a user-agent to tamper with. If the user is not signed in they are sent to login first and returned to the same consent URL. The consent screen then calls `GET /oauth/authorize-request/{id}` and one of approve/deny. Errors before the redirect URI is validated are rendered as an RFC 6749 error body (an unregistered URI must never be redirected to, or the error path becomes an open redirect); errors after it are appended to the registered redirect URI as `error`/`error_description` with `state` echoed back. Rate limited per `client_id`.","security":[],"parameters":[{"name":"client_id","in":"query","required":true,"description":"From `POST /oauth/register` (`ocli_…`).","schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"description":"Must exactly match one of the URIs registered for this client.","schema":{"type":"string"}},{"name":"response_type","in":"query","required":true,"description":"Only `code` is supported.","schema":{"type":"string","enum":["code"]}},{"name":"code_challenge","in":"query","required":true,"description":"PKCE challenge: base64url, 43–128 characters.","schema":{"type":"string","minLength":43,"maxLength":128}},{"name":"code_challenge_method","in":"query","required":true,"description":"Must be `S256`. `plain` and an absent value are both rejected.","schema":{"type":"string","enum":["S256"]}},{"name":"scope","in":"query","required":false,"description":"MCP scopes, space-delimited on the wire. `mcp:read` reads org data; `mcp:write` creates and changes it; `mcp:destructive` deletes and removes access; `mcp:tools` invokes the third-party tools the user has connected. `openid` and `email` are identity scopes: they authorize `GET/POST /oauth/userinfo` and grant no MCP access on their own — a grant holding only `openid email` reaches no `mcp:*` operation and no connected tool. Requesting `mcp:write` or `mcp:destructive` implies `mcp:read` — a client that can mutate what it cannot read is not a useful capability. Unknown scope values are rejected rather than silently dropped. A request with no `scope` at all defaults to `mcp:read`.","schema":{"type":"string"}},{"name":"state","in":"query","required":false,"description":"Opaque client value, echoed on every redirect back.","schema":{"type":"string"}},{"name":"resource","in":"query","required":false,"description":"RFC 8707 audience. Must identify this server by origin; a resource on another origin is an audience-confusion attempt and is rejected with `invalid_target`.","schema":{"type":"string","format":"uri"}}]}},"/oauth/token":{"post":{"operationId":"issueOAuthToken","summary":"Exchange a code or refresh token for an access token","tags":["MCP OAuth"],"responses":{"200":{"description":"Tokens issued","content":{"application/json":{"schema":{"type":"object","required":["access_token","token_type","expires_in","refresh_token","scope"],"properties":{"access_token":{"type":"string","description":"Bearer token for `POST /mcp`. Short-lived; re-check `expires_in` rather than assuming."},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer","description":"Access token lifetime in seconds."},"refresh_token":{"type":"string","description":"Single-use — rotated on every refresh."},"scope":{"type":"string","description":"Space-delimited granted scopes."}}}}}},"400":{"description":"`invalid_request`, `invalid_grant` (including replay, which also revoked the grant), `invalid_scope`, `invalid_target` or `unsupported_grant_type`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"`invalid_client` — unknown client, or client authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (`temporarily_unavailable`), with `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}},"description":"Accepts `application/x-www-form-urlencoded` (RFC 6749) or `application/json`. Public clients authenticate with PKCE alone; confidential clients MUST present their secret, by HTTP Basic or `client_secret` in the body. \n\n`grant_type=authorization_code` requires `code`, `redirect_uri` and `code_verifier`. The redirect URI and the client must match those the code was issued to.\n\n`grant_type=refresh_token` requires `refresh_token`, and rotates it — the presented token is consumed and a new one returned. An optional `scope` may narrow the grant, never widen it; the grant row is the authority, not the copy on the presented token.\n\nReplay is treated as compromise, not as a plain error: presenting an authorization code or a refresh token twice REVOKES THE WHOLE GRANT (OAuth 2.1 §4.1.3 / §6.1), invalidating every token derived from it. The user must re-authorize from `GET /oauth/authorize`. Responses are `Cache-Control: no-store`. Rate limited per `client_id`.","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"client_id":{"type":"string"},"client_secret":{"type":"string","description":"Confidential clients only; HTTP Basic is also accepted."},"code":{"type":"string","description":"authorization_code grant."},"redirect_uri":{"type":"string","description":"authorization_code grant; must match the code."},"code_verifier":{"type":"string","description":"authorization_code grant; PKCE verifier for the S256 challenge."},"refresh_token":{"type":"string","description":"refresh_token grant."},"scope":{"type":"string","description":"refresh_token grant; may only narrow the grant."},"resource":{"type":"string","description":"RFC 8707 audience; must identify this server."}}}},"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}},"/oauth/revoke":{"post":{"operationId":"revokeOAuthToken","summary":"Revoke a token (RFC 7009)","tags":["MCP OAuth"],"responses":{"200":{"description":"Always returned on a well-formed, client-authenticated request. Body is `{}`.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"`invalid_request` — `token` is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"`invalid_client` — client authentication failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited (`temporarily_unavailable`), with `Retry-After`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}},"description":"Revoking either an access token or a refresh token kills the ENTIRE grant it belongs to, not just the token presented — a client the user disconnected must not keep a live refresh token. Accepts form-encoded or JSON and authenticates the client exactly as `POST /oauth/token` does. Always answers 200, including for unknown, already-revoked, or another client's token, so this endpoint cannot be used as a token oracle. To revoke from the user side instead, use `DELETE /oauth/grant/{id}`.","security":[],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"An access token or a refresh token."},"token_type_hint":{"type":"string","description":"Accepted and ignored; both types are looked up."},"client_id":{"type":"string"},"client_secret":{"type":"string"}}}},"application/json":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}},"/oauth/userinfo":{"get":{"operationId":"getUserinfo","summary":"UserInfo endpoint (OIDC Core §5.3)","tags":["MCP OAuth"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["sub"],"properties":{"sub":{"type":"string","description":"Stable, opaque subject identifier (`usr_…`). Same value for every client — `subject_types_supported: [\"public\"]`."},"email":{"type":"string","format":"email","description":"Present only when the grant holds the `email` scope."},"email_verified":{"type":"boolean","description":"Present only alongside `email`. Derived from how the account authenticates (a consumed email code, a signed SSO assertion over a DNS-verified org domain, an identity provider that publishes and enforces its own verification signal) — never a self-reported flag."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential supplied → `invalid_request`, no `error` parameter on the challenge (RFC 6750 §3.1: a request carrying no credential has failed nothing yet). A bad, expired or revoked credential → `invalid_token`, with `error=\"invalid_token\"` on the challenge.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}},"headers":{"WWW-Authenticate":{"description":"RFC 6750 §3 challenge, `error`/`error_description` present only when a credential was supplied.","schema":{"type":"string"}}}},"403":{"description":"A valid token whose grant does not hold the `openid` scope. `insufficient_scope` rather than 401 — re-authenticating would not help, the client must send the user back through consent for more.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}},"headers":{"WWW-Authenticate":{"description":"Carries `error=\"insufficient_scope\"` and `scope=\"openid\"`.","schema":{"type":"string"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Returns claims about the user who authorized the grant behind the bearer token. Bearer-authenticated by the same `verifyMcpAccessToken` function `POST /mcp` uses, so a revoked grant, a deactivated user and an expired token are all refused exactly as they are there. Never cached: this is the one response in this router that can carry a person's address.\n\n`sub` is always present — the stable `usr_…` primary key, never reassigned and never pairwise. `email` and `email_verified` are returned together, and only when the grant holds the `email` scope; a client that asked only for `openid` gets a subject and no contact detail. The two never appear separately, since an `email` without the flag beside it would invite a relying party to assume verification.","security":[{"bearerAuth":[]}]},"post":{"operationId":"postUserinfo","summary":"UserInfo endpoint (OIDC Core §5.3)","tags":["MCP OAuth"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["sub"],"properties":{"sub":{"type":"string","description":"Stable, opaque subject identifier (`usr_…`). Same value for every client — `subject_types_supported: [\"public\"]`."},"email":{"type":"string","format":"email","description":"Present only when the grant holds the `email` scope."},"email_verified":{"type":"boolean","description":"Present only alongside `email`. Derived from how the account authenticates (a consumed email code, a signed SSO assertion over a DNS-verified org domain, an identity provider that publishes and enforces its own verification signal) — never a self-reported flag."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No credential supplied → `invalid_request`, no `error` parameter on the challenge (RFC 6750 §3.1: a request carrying no credential has failed nothing yet). A bad, expired or revoked credential → `invalid_token`, with `error=\"invalid_token\"` on the challenge.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}},"headers":{"WWW-Authenticate":{"description":"RFC 6750 §3 challenge, `error`/`error_description` present only when a credential was supplied.","schema":{"type":"string"}}}},"403":{"description":"A valid token whose grant does not hold the `openid` scope. `insufficient_scope` rather than 401 — re-authenticating would not help, the client must send the user back through consent for more.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}},"headers":{"WWW-Authenticate":{"description":"Carries `error=\"insufficient_scope\"` and `scope=\"openid\"`.","schema":{"type":"string"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Returns claims about the user who authorized the grant behind the bearer token. Bearer-authenticated by the same `verifyMcpAccessToken` function `POST /mcp` uses, so a revoked grant, a deactivated user and an expired token are all refused exactly as they are there. Never cached: this is the one response in this router that can carry a person's address.\n\n`sub` is always present — the stable `usr_…` primary key, never reassigned and never pairwise. `email` and `email_verified` are returned together, and only when the grant holds the `email` scope; a client that asked only for `openid` gets a subject and no contact detail. The two never appear separately, since an `email` without the flag beside it would invite a relying party to assume verification.","security":[{"bearerAuth":[]}]}},"/restriction":{"get":{"operationId":"listRestrictions","summary":"List restrictions","tags":["Restrictions"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Restriction id (`rstr_…`)."},"target_type":{"type":"string","enum":["role","user"],"description":"Whether this rule applies to everyone holding a role, or to one specific member."},"target_id":{"type":"string","description":"Role id (`role_…`) or user id (`usr_…`), matching `target_type`."},"mode":{"type":"string","enum":["allow","block"],"description":"`allow` makes the listed connectors and tools the ONLY ones the target may use; `block` removes exactly the listed ones and leaves everything else reachable. The allowlist engages on the MODE, not on what the rule names: an `allow` rule with empty `connector_slugs` and empty `tools` blocks every connector and every tool for its target."},"connector_slugs":{"type":"array","items":{"type":"string"},"description":"Whole connectors covered by the rule. A slug here settles every tool on that connector, so it must not also appear as a `tools[].connector_slug`."},"tools":{"type":"array","items":{"type":"object","required":["connector_slug","tool_name"],"properties":{"connector_slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":"string","description":"Exact tool name from `GET /connector/{slug}/tools`."},"tool_label":{"type":"string","description":"Person-readable phrase for this tool (\"create a contact\"), computed server-side from `tool_name`. Read-only. Render this instead of the raw id, and never rebuild it client-side — the humanizer lives on the server and a second copy drifts from it. The connector is deliberately not folded in: it is right beside this as `connector_slug`."}}},"description":"Individual tools covered by the rule, on connectors `connector_slugs` does not already name whole."},"summary":{"type":"string","description":"One plain sentence saying what this rule does (\"Allows botify, plus 2 tools in slack. Everything else is blocked.\"), computed server-side so every client says the same thing. Read-only, derived from `mode`/`connector_slugs`/`tools`, never stored. It describes what is ENFORCED: tool entries on a connector already named whole are inert, so they do not appear in it. Connectors are named by slug — resolving a display name would be one catalog read per slug per row. Render this rather than composing your own; a client-derived summary is how \"an allow rule that names nothing\" came to read as \"All connectors\"."},"created_by":{"type":["string","null"],"description":"User id (`usr_…`) of the author."},"created_by_label":{"type":["string","null"],"description":"Display name (or email) of the author, resolved server-side in the SAME batch as `target_label` — one query for the whole page, so no client ever looks an author up row by row. `null` while `created_by` is a real id means the author has LEFT the org (the resolver joins through the org's own member index), which is a fact about the person, not a lookup that failed — render it as \"Former member\", the same word `GET /audit-log`'s `actor` summary uses. Also `null` when `created_by` itself is."},"updated_by":{"type":["string","null"],"description":"User id (`usr_…`) of whoever wrote the row LAST — the author until somebody else edits it. Null only on a row written before this field existed; the self-target rule reads that as \"not you\", so such a row constrains its author like anyone else's."},"updated_by_label":{"type":["string","null"],"description":"Display name (or email) for `updated_by`, resolved in the same one batch as `created_by_label`, with the same \"Former member\" meaning for a null beside a real id. Compare the two IDS, never the two labels, to decide whether somebody other than the author last wrote the row: two people can share a display name, and both labels are null once both have left."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"target_label":{"type":["string","null"],"description":"Display name (or email) of a `user` target, resolved server-side one batch per page so the console never looks members up row by row. Always `null` on a `role` target — role names live in the org DO, which that batch cannot reach, so the console resolves them against the role catalog it already holds — and `null` on a stale target whose member has been deleted."},"can_manage":{"type":"boolean","description":"Whether the caller may create, edit or delete THIS row — `restriction:manage`, plus the two guards the write routes themselves enforce: a user-targeted row additionally needs `restriction:override`, and a row targeting the caller (or a role whose permission set equals theirs exactly) is refused so nobody can free themselves from a restriction. Computed by calling those same guard functions, never re-derived, so a row showing `can_manage: true` cannot 403 on write. Read routes need only `restriction:view`, so this is `false` for a viewer."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `restriction:view`. Restrictions clamp what connectors and tools a target may reach anywhere — the assistant, MCP endpoints and `GET /connection/{id}/tools` all apply them.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"post":{"operationId":"createRestriction","summary":"Create a restriction","tags":["Restrictions"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Restriction id (`rstr_…`)."},"target_type":{"type":"string","enum":["role","user"],"description":"Whether this rule applies to everyone holding a role, or to one specific member."},"target_id":{"type":"string","description":"Role id (`role_…`) or user id (`usr_…`), matching `target_type`."},"mode":{"type":"string","enum":["allow","block"],"description":"`allow` makes the listed connectors and tools the ONLY ones the target may use; `block` removes exactly the listed ones and leaves everything else reachable. The allowlist engages on the MODE, not on what the rule names: an `allow` rule with empty `connector_slugs` and empty `tools` blocks every connector and every tool for its target."},"connector_slugs":{"type":"array","items":{"type":"string"},"description":"Whole connectors covered by the rule. A slug here settles every tool on that connector, so it must not also appear as a `tools[].connector_slug`."},"tools":{"type":"array","items":{"type":"object","required":["connector_slug","tool_name"],"properties":{"connector_slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":"string","description":"Exact tool name from `GET /connector/{slug}/tools`."},"tool_label":{"type":"string","description":"Person-readable phrase for this tool (\"create a contact\"), computed server-side from `tool_name`. Read-only. Render this instead of the raw id, and never rebuild it client-side — the humanizer lives on the server and a second copy drifts from it. The connector is deliberately not folded in: it is right beside this as `connector_slug`."}}},"description":"Individual tools covered by the rule, on connectors `connector_slugs` does not already name whole."},"summary":{"type":"string","description":"One plain sentence saying what this rule does (\"Allows botify, plus 2 tools in slack. Everything else is blocked.\"), computed server-side so every client says the same thing. Read-only, derived from `mode`/`connector_slugs`/`tools`, never stored. It describes what is ENFORCED: tool entries on a connector already named whole are inert, so they do not appear in it. Connectors are named by slug — resolving a display name would be one catalog read per slug per row. Render this rather than composing your own; a client-derived summary is how \"an allow rule that names nothing\" came to read as \"All connectors\"."},"created_by":{"type":["string","null"],"description":"User id (`usr_…`) of the author."},"created_by_label":{"type":["string","null"],"description":"Display name (or email) of the author, resolved server-side in the SAME batch as `target_label` — one query for the whole page, so no client ever looks an author up row by row. `null` while `created_by` is a real id means the author has LEFT the org (the resolver joins through the org's own member index), which is a fact about the person, not a lookup that failed — render it as \"Former member\", the same word `GET /audit-log`'s `actor` summary uses. Also `null` when `created_by` itself is."},"updated_by":{"type":["string","null"],"description":"User id (`usr_…`) of whoever wrote the row LAST — the author until somebody else edits it. Null only on a row written before this field existed; the self-target rule reads that as \"not you\", so such a row constrains its author like anyone else's."},"updated_by_label":{"type":["string","null"],"description":"Display name (or email) for `updated_by`, resolved in the same one batch as `created_by_label`, with the same \"Former member\" meaning for a null beside a real id. Compare the two IDS, never the two labels, to decide whether somebody other than the author last wrote the row: two people can share a display name, and both labels are null once both have left."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"target_label":{"type":["string","null"],"description":"Display name (or email) of a `user` target, resolved server-side one batch per page so the console never looks members up row by row. Always `null` on a `role` target — role names live in the org DO, which that batch cannot reach, so the console resolves them against the role catalog it already holds — and `null` on a stale target whose member has been deleted."},"can_manage":{"type":"boolean","description":"Whether the caller may create, edit or delete THIS row — `restriction:manage`, plus the two guards the write routes themselves enforce: a user-targeted row additionally needs `restriction:override`, and a row targeting the caller (or a role whose permission set equals theirs exactly) is refused so nobody can free themselves from a restriction. Computed by calling those same guard functions, never re-derived, so a row showing `can_manage: true` cannot 403 on write. Read routes need only `restriction:view`, so this is `false` for a viewer."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `restriction:manage` and the `restrictions` plan feature. A `user` target additionally requires `restriction:override`: a user-targeted rule REPLACES every role rule for that member, so it can loosen governance as well as tighten it. Changes take effect within about a minute, once the org rule cache turns over.\n\n**A connector named in `connector_slugs` must not also appear as a `tools[].connector_slug`** — `400`, naming the connector. Restricting a whole connector already settles every tool on it, so the tool entries change nothing; on an `allow` rule they are worse than nothing, because the rule then grants the WHOLE connector rather than the tools that were picked. Read `summary` back to see what the rule you just wrote actually does.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["target_type","target_id","mode"],"properties":{"target_type":{"type":"string","enum":["role","user"]},"target_id":{"type":"string","description":"Role id (`role_…`) or user id (`usr_…`)."},"mode":{"type":"string","enum":["allow","block"]},"connector_slugs":{"type":"array","maxItems":1000,"items":{"type":"string"},"description":"Whole connectors the rule covers. Defaults to empty — which on an `allow` rule with no `tools` either means the rule blocks EVERY connector and tool for its target, not that it allows them all. The allowlist engages on `mode`, not on what the rule names."},"tools":{"type":"array","maxItems":1000,"items":{"type":"object","required":["connector_slug","tool_name"],"properties":{"connector_slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":"string","description":"Exact tool name from `GET /connector/{slug}/tools`."}}},"description":"Individual tools the rule covers. Defaults to empty. A `connector_slug` here must not also be in `connector_slugs`."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/restriction/{id}":{"get":{"operationId":"getRestriction","summary":"Get a restriction","tags":["Restrictions"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Restriction id (`rstr_…`)."},"target_type":{"type":"string","enum":["role","user"],"description":"Whether this rule applies to everyone holding a role, or to one specific member."},"target_id":{"type":"string","description":"Role id (`role_…`) or user id (`usr_…`), matching `target_type`."},"mode":{"type":"string","enum":["allow","block"],"description":"`allow` makes the listed connectors and tools the ONLY ones the target may use; `block` removes exactly the listed ones and leaves everything else reachable. The allowlist engages on the MODE, not on what the rule names: an `allow` rule with empty `connector_slugs` and empty `tools` blocks every connector and every tool for its target."},"connector_slugs":{"type":"array","items":{"type":"string"},"description":"Whole connectors covered by the rule. A slug here settles every tool on that connector, so it must not also appear as a `tools[].connector_slug`."},"tools":{"type":"array","items":{"type":"object","required":["connector_slug","tool_name"],"properties":{"connector_slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":"string","description":"Exact tool name from `GET /connector/{slug}/tools`."},"tool_label":{"type":"string","description":"Person-readable phrase for this tool (\"create a contact\"), computed server-side from `tool_name`. Read-only. Render this instead of the raw id, and never rebuild it client-side — the humanizer lives on the server and a second copy drifts from it. The connector is deliberately not folded in: it is right beside this as `connector_slug`."}}},"description":"Individual tools covered by the rule, on connectors `connector_slugs` does not already name whole."},"summary":{"type":"string","description":"One plain sentence saying what this rule does (\"Allows botify, plus 2 tools in slack. Everything else is blocked.\"), computed server-side so every client says the same thing. Read-only, derived from `mode`/`connector_slugs`/`tools`, never stored. It describes what is ENFORCED: tool entries on a connector already named whole are inert, so they do not appear in it. Connectors are named by slug — resolving a display name would be one catalog read per slug per row. Render this rather than composing your own; a client-derived summary is how \"an allow rule that names nothing\" came to read as \"All connectors\"."},"created_by":{"type":["string","null"],"description":"User id (`usr_…`) of the author."},"created_by_label":{"type":["string","null"],"description":"Display name (or email) of the author, resolved server-side in the SAME batch as `target_label` — one query for the whole page, so no client ever looks an author up row by row. `null` while `created_by` is a real id means the author has LEFT the org (the resolver joins through the org's own member index), which is a fact about the person, not a lookup that failed — render it as \"Former member\", the same word `GET /audit-log`'s `actor` summary uses. Also `null` when `created_by` itself is."},"updated_by":{"type":["string","null"],"description":"User id (`usr_…`) of whoever wrote the row LAST — the author until somebody else edits it. Null only on a row written before this field existed; the self-target rule reads that as \"not you\", so such a row constrains its author like anyone else's."},"updated_by_label":{"type":["string","null"],"description":"Display name (or email) for `updated_by`, resolved in the same one batch as `created_by_label`, with the same \"Former member\" meaning for a null beside a real id. Compare the two IDS, never the two labels, to decide whether somebody other than the author last wrote the row: two people can share a display name, and both labels are null once both have left."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"target_label":{"type":["string","null"],"description":"Display name (or email) of a `user` target, resolved server-side one batch per page so the console never looks members up row by row. Always `null` on a `role` target — role names live in the org DO, which that batch cannot reach, so the console resolves them against the role catalog it already holds — and `null` on a stale target whose member has been deleted."},"can_manage":{"type":"boolean","description":"Whether the caller may create, edit or delete THIS row — `restriction:manage`, plus the two guards the write routes themselves enforce: a user-targeted row additionally needs `restriction:override`, and a row targeting the caller (or a role whose permission set equals theirs exactly) is refused so nobody can free themselves from a restriction. Computed by calling those same guard functions, never re-derived, so a row showing `can_manage: true` cannot 403 on write. Read routes need only `restriction:view`, so this is `false` for a viewer."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `restriction:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Restriction id (`rstr_…`)."}]},"patch":{"operationId":"updateRestriction","summary":"Update a restriction","tags":["Restrictions"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Restriction id (`rstr_…`)."},"target_type":{"type":"string","enum":["role","user"],"description":"Whether this rule applies to everyone holding a role, or to one specific member."},"target_id":{"type":"string","description":"Role id (`role_…`) or user id (`usr_…`), matching `target_type`."},"mode":{"type":"string","enum":["allow","block"],"description":"`allow` makes the listed connectors and tools the ONLY ones the target may use; `block` removes exactly the listed ones and leaves everything else reachable. The allowlist engages on the MODE, not on what the rule names: an `allow` rule with empty `connector_slugs` and empty `tools` blocks every connector and every tool for its target."},"connector_slugs":{"type":"array","items":{"type":"string"},"description":"Whole connectors covered by the rule. A slug here settles every tool on that connector, so it must not also appear as a `tools[].connector_slug`."},"tools":{"type":"array","items":{"type":"object","required":["connector_slug","tool_name"],"properties":{"connector_slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":"string","description":"Exact tool name from `GET /connector/{slug}/tools`."},"tool_label":{"type":"string","description":"Person-readable phrase for this tool (\"create a contact\"), computed server-side from `tool_name`. Read-only. Render this instead of the raw id, and never rebuild it client-side — the humanizer lives on the server and a second copy drifts from it. The connector is deliberately not folded in: it is right beside this as `connector_slug`."}}},"description":"Individual tools covered by the rule, on connectors `connector_slugs` does not already name whole."},"summary":{"type":"string","description":"One plain sentence saying what this rule does (\"Allows botify, plus 2 tools in slack. Everything else is blocked.\"), computed server-side so every client says the same thing. Read-only, derived from `mode`/`connector_slugs`/`tools`, never stored. It describes what is ENFORCED: tool entries on a connector already named whole are inert, so they do not appear in it. Connectors are named by slug — resolving a display name would be one catalog read per slug per row. Render this rather than composing your own; a client-derived summary is how \"an allow rule that names nothing\" came to read as \"All connectors\"."},"created_by":{"type":["string","null"],"description":"User id (`usr_…`) of the author."},"created_by_label":{"type":["string","null"],"description":"Display name (or email) of the author, resolved server-side in the SAME batch as `target_label` — one query for the whole page, so no client ever looks an author up row by row. `null` while `created_by` is a real id means the author has LEFT the org (the resolver joins through the org's own member index), which is a fact about the person, not a lookup that failed — render it as \"Former member\", the same word `GET /audit-log`'s `actor` summary uses. Also `null` when `created_by` itself is."},"updated_by":{"type":["string","null"],"description":"User id (`usr_…`) of whoever wrote the row LAST — the author until somebody else edits it. Null only on a row written before this field existed; the self-target rule reads that as \"not you\", so such a row constrains its author like anyone else's."},"updated_by_label":{"type":["string","null"],"description":"Display name (or email) for `updated_by`, resolved in the same one batch as `created_by_label`, with the same \"Former member\" meaning for a null beside a real id. Compare the two IDS, never the two labels, to decide whether somebody other than the author last wrote the row: two people can share a display name, and both labels are null once both have left."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"target_label":{"type":["string","null"],"description":"Display name (or email) of a `user` target, resolved server-side one batch per page so the console never looks members up row by row. Always `null` on a `role` target — role names live in the org DO, which that batch cannot reach, so the console resolves them against the role catalog it already holds — and `null` on a stale target whose member has been deleted."},"can_manage":{"type":"boolean","description":"Whether the caller may create, edit or delete THIS row — `restriction:manage`, plus the two guards the write routes themselves enforce: a user-targeted row additionally needs `restriction:override`, and a row targeting the caller (or a role whose permission set equals theirs exactly) is refused so nobody can free themselves from a restriction. Computed by calling those same guard functions, never re-derived, so a row showing `can_manage: true` cannot 403 on write. Read routes need only `restriction:view`, so this is `false` for a viewer."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Merge-patch at the top level, but **`connector_slugs` and `tools` are each REPLACED wholesale when present** — send the complete list, since anything omitted is dropped from the rule. Same permissions as create, including `restriction:override` for user targets.\n\nThe connector/tool overlap rule from `POST` applies to the resulting rule, with one exemption: an overlap the row ALREADY had is left alone, so a row written before the rule existed stays editable through a rename, a mode flip or a target change that echoes both arrays back. Only overlap this patch ADDS is a `400`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"target_type":{"type":"string","enum":["role","user"]},"target_id":{"type":"string"},"mode":{"type":"string","enum":["allow","block"]},"connector_slugs":{"type":"array","maxItems":1000,"items":{"type":"string"},"description":"Replaces the list."},"tools":{"type":"array","maxItems":1000,"items":{"type":"object","required":["connector_slug","tool_name"],"properties":{"connector_slug":{"type":"string","description":"Connector slug — connectors are keyed by slug, not by id."},"tool_name":{"type":"string","description":"Exact tool name from `GET /connector/{slug}/tools`."}}},"description":"Replaces the list."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Restriction id (`rstr_…`)."}]},"delete":{"operationId":"deleteRestriction","summary":"Delete a restriction","tags":["Restrictions"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Removes the clamp: access reverts to whatever the remaining rules allow, which for an `allow` rule means the target regains access to everything else. Requires `restriction:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Restriction id (`rstr_…`)."}]}},"/access-request":{"get":{"operationId":"listAccessRequests","summary":"List access requests (admin queue, or your own)","tags":["Access requests"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"oneOf":[{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction (`BlockedAppPayload.can_request_access`). Filing or resolving one never grants anything by itself — it is a record for a human to act on through the existing role/restriction APIs.","properties":{"id":{"type":"string","description":"Access request id (`areq_…`)."},"requester_user_id":{"type":"string","description":"User id (`usr_…`) of whoever filed the request."},"tool":{"type":"string","description":"The tool name the requester was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"],"description":"What kind of refusal this request is asking to be reconsidered."},"permission":{"type":["string","null"],"description":"Present only when `reason` is `\"permission\"` — never populated for a `\"restriction\"`-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact."},"note":{"type":["string","null"],"description":"Optional free text from the requester, ≤ 2000 characters."},"status":{"type":"string","enum":["pending","approved","denied","withdrawn"]},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by_user_id":{"type":["string","null"],"description":"User id (`usr_…`) of the admin who resolved it."},"resolution_note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","description":"The admin queue’s row shape — returned by `GET /access-request`, `GET /access-request/{id}` when the caller holds `member:manage`, and `POST /access-request/{id}/resolve`.","properties":{"requester":{"type":"object","description":"Resolved member profile. Falls back to `{ id }` alone when the profile row no longer resolves.","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"resolved_by":{"type":["object","null"],"description":"Resolved member profile. Falls back to `{ id }` alone when the profile row no longer resolves.","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"can_resolve":{"type":"boolean","description":"True while `status` is still `pending` **and** the row is not the caller’s own — the two preconditions `resolve` itself enforces (`409` and `403` respectively)."},"can_withdraw":{"type":"boolean","description":"True when the caller is the requester and `status` is still `pending`. Present on this shape as well as on the self view: an admin’s own request sits in their own queue, and withdrawing it is the one verb on that row that is theirs."}}}]},{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction (`BlockedAppPayload.can_request_access`). Filing or resolving one never grants anything by itself — it is a record for a human to act on through the existing role/restriction APIs.","properties":{"id":{"type":"string","description":"Access request id (`areq_…`)."},"requester_user_id":{"type":"string","description":"User id (`usr_…`) of whoever filed the request."},"tool":{"type":"string","description":"The tool name the requester was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"],"description":"What kind of refusal this request is asking to be reconsidered."},"permission":{"type":["string","null"],"description":"Present only when `reason` is `\"permission\"` — never populated for a `\"restriction\"`-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact."},"note":{"type":["string","null"],"description":"Optional free text from the requester, ≤ 2000 characters."},"status":{"type":"string","enum":["pending","approved","denied","withdrawn"]},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by_user_id":{"type":["string","null"],"description":"User id (`usr_…`) of the admin who resolved it."},"resolution_note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","description":"The requester’s own view — returned by `POST /access-request`, `POST /access-request/{id}/withdraw`, and `GET /access-request/{id}` when the caller is the request’s own requester. `resolved_by_user_id` stays a bare id here: resolving it into a name/email is a member-directory lookup this view does not hand a plain member for free.","properties":{"can_withdraw":{"type":"boolean","description":"True when the caller is the requester and `status` is still `pending`."}}}]}],"description":"Admin view by default; self view when `mine=true`."}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Two audiences on one route. **Without `mine`** this is the org-wide admin queue and requires `member:manage` — the closest existing permission to \"may decide who gets more access\": acting on a request always ends in a role change, a restriction edit, or an invite, so no new permission was added for this resource. A caller without it gets `403`, never an empty page — \"nothing here\" is a claim about the organization that a bystander has not earned. Rows are the admin view (`requester`, `resolved_by`, `can_resolve`).\n\n**With `mine=true`** it answers only the CALLER’S OWN requests and needs no permission at all — the same nothing that filing one needed, for the same deadlock reason, and the only way the person who filed a request can find it again. The scope comes from the session, so no id on the wire can name somebody else. Rows are the narrower **self view** (`can_withdraw`, and a bare `resolved_by_user_id` rather than a resolved name) for every caller, admins included: resolving an id into a name and email is a member-directory lookup this project does not hang off a route a plain member can reach.\n\n`limit`, `cursor`, `q` and `status` apply identically to both. `q` matches the requested **tool** name or the requester’s **note**, not the requester themselves — search members by name with a member lookup and cross-reference `requester_user_id` instead. It does not match `tool_label`: a connector’s label is resolved outside the store and lands in no column the query can read.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the requested tool name or the requester’s note. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"status","in":"query","required":false,"description":"Filter by status. Defaults to `pending`. Any other value is a 400.","schema":{"type":"string","enum":["pending","approved","denied","withdrawn","all"]}},{"name":"mine","in":"query","required":false,"description":"Set `true` to list only the requests you filed (no permission required, self-view rows). Omitted or `false` means the org-wide admin queue, which requires `member:manage`. Any other value is a 400, so a typo cannot silently fall through to the queue.","schema":{"type":"string","enum":["true","false","1","0"]}}]},"post":{"operationId":"createAccessRequest","summary":"File an access request","tags":["Access requests"],"responses":{"200":{"description":"De-duplicated onto an existing pending request","content":{"application/json":{"schema":{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction (`BlockedAppPayload.can_request_access`). Filing or resolving one never grants anything by itself — it is a record for a human to act on through the existing role/restriction APIs.","properties":{"id":{"type":"string","description":"Access request id (`areq_…`)."},"requester_user_id":{"type":"string","description":"User id (`usr_…`) of whoever filed the request."},"tool":{"type":"string","description":"The tool name the requester was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"],"description":"What kind of refusal this request is asking to be reconsidered."},"permission":{"type":["string","null"],"description":"Present only when `reason` is `\"permission\"` — never populated for a `\"restriction\"`-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact."},"note":{"type":["string","null"],"description":"Optional free text from the requester, ≤ 2000 characters."},"status":{"type":"string","enum":["pending","approved","denied","withdrawn"]},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by_user_id":{"type":["string","null"],"description":"User id (`usr_…`) of the admin who resolved it."},"resolution_note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","description":"The requester’s own view — returned by `POST /access-request`, `POST /access-request/{id}/withdraw`, and `GET /access-request/{id}` when the caller is the request’s own requester. `resolved_by_user_id` stays a bare id here: resolving it into a name/email is a member-directory lookup this view does not hand a plain member for free.","properties":{"can_withdraw":{"type":"boolean","description":"True when the caller is the requester and `status` is still `pending`."}}}]}}}},"201":{"description":"Created a new request","content":{"application/json":{"schema":{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction (`BlockedAppPayload.can_request_access`). Filing or resolving one never grants anything by itself — it is a record for a human to act on through the existing role/restriction APIs.","properties":{"id":{"type":"string","description":"Access request id (`areq_…`)."},"requester_user_id":{"type":"string","description":"User id (`usr_…`) of whoever filed the request."},"tool":{"type":"string","description":"The tool name the requester was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"],"description":"What kind of refusal this request is asking to be reconsidered."},"permission":{"type":["string","null"],"description":"Present only when `reason` is `\"permission\"` — never populated for a `\"restriction\"`-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact."},"note":{"type":["string","null"],"description":"Optional free text from the requester, ≤ 2000 characters."},"status":{"type":"string","enum":["pending","approved","denied","withdrawn"]},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by_user_id":{"type":["string","null"],"description":"User id (`usr_…`) of the admin who resolved it."},"resolution_note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","description":"The requester’s own view — returned by `POST /access-request`, `POST /access-request/{id}/withdraw`, and `GET /access-request/{id}` when the caller is the request’s own requester. `resolved_by_user_id` stays a bare id here: resolving it into a name/email is a member-directory lookup this view does not hand a plain member for free.","properties":{"can_withdraw":{"type":"boolean","description":"True when the caller is the requester and `status` is still `pending`."}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Any authenticated member — no permission required. Requiring a permission to ask for a permission would be a deadlock: the member who most needs this route holds the fewest permissions. Membership (`orgContext()`) is the only gate, and there is no `requester_user_id` in the body — a request is always filed as the caller. `permission` is required and must be a known permission name when `reason` is `\"permission\"`, and forbidden (a `400`) when `reason` is `\"restriction\"` — the same disclosure rule a restriction refusal already enforces on the refusal itself: this resource must not become a second way to learn which rule blocked someone. `reason` is never `\"scope\"` — a scope refusal has no admin-side fix and never sets `can_request_access`.\n\n**Filing never grants anything.** This only ever creates a record for an admin to act on through the existing role/restriction APIs.\n\n**De-duplicated.** A second request for the same `tool` while an earlier one from the same requester is still `pending` does not create a new row — it returns the existing one (updating `note` when a different one was sent) with `200`. A genuinely new row is `201`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tool","reason"],"properties":{"tool":{"type":"string","minLength":1,"maxLength":200,"description":"The tool name the caller was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"]},"permission":{"type":"string","maxLength":64,"description":"Required when `reason` is `\"permission\"`; forbidden (400) when `reason` is `\"restriction\"`."},"note":{"type":"string","maxLength":2000}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/access-request/{id}":{"get":{"operationId":"getAccessRequest","summary":"Get an access request","tags":["Access requests"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"oneOf":[{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction (`BlockedAppPayload.can_request_access`). Filing or resolving one never grants anything by itself — it is a record for a human to act on through the existing role/restriction APIs.","properties":{"id":{"type":"string","description":"Access request id (`areq_…`)."},"requester_user_id":{"type":"string","description":"User id (`usr_…`) of whoever filed the request."},"tool":{"type":"string","description":"The tool name the requester was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"],"description":"What kind of refusal this request is asking to be reconsidered."},"permission":{"type":["string","null"],"description":"Present only when `reason` is `\"permission\"` — never populated for a `\"restriction\"`-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact."},"note":{"type":["string","null"],"description":"Optional free text from the requester, ≤ 2000 characters."},"status":{"type":"string","enum":["pending","approved","denied","withdrawn"]},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by_user_id":{"type":["string","null"],"description":"User id (`usr_…`) of the admin who resolved it."},"resolution_note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","description":"The requester’s own view — returned by `POST /access-request`, `POST /access-request/{id}/withdraw`, and `GET /access-request/{id}` when the caller is the request’s own requester. `resolved_by_user_id` stays a bare id here: resolving it into a name/email is a member-directory lookup this view does not hand a plain member for free.","properties":{"can_withdraw":{"type":"boolean","description":"True when the caller is the requester and `status` is still `pending`."}}}]},{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction (`BlockedAppPayload.can_request_access`). Filing or resolving one never grants anything by itself — it is a record for a human to act on through the existing role/restriction APIs.","properties":{"id":{"type":"string","description":"Access request id (`areq_…`)."},"requester_user_id":{"type":"string","description":"User id (`usr_…`) of whoever filed the request."},"tool":{"type":"string","description":"The tool name the requester was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"],"description":"What kind of refusal this request is asking to be reconsidered."},"permission":{"type":["string","null"],"description":"Present only when `reason` is `\"permission\"` — never populated for a `\"restriction\"`-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact."},"note":{"type":["string","null"],"description":"Optional free text from the requester, ≤ 2000 characters."},"status":{"type":"string","enum":["pending","approved","denied","withdrawn"]},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by_user_id":{"type":["string","null"],"description":"User id (`usr_…`) of the admin who resolved it."},"resolution_note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","description":"The admin queue’s row shape — returned by `GET /access-request`, `GET /access-request/{id}` when the caller holds `member:manage`, and `POST /access-request/{id}/resolve`.","properties":{"requester":{"type":"object","description":"Resolved member profile. Falls back to `{ id }` alone when the profile row no longer resolves.","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"resolved_by":{"type":["object","null"],"description":"Resolved member profile. Falls back to `{ id }` alone when the profile row no longer resolves.","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"can_resolve":{"type":"boolean","description":"True while `status` is still `pending` **and** the row is not the caller’s own — the two preconditions `resolve` itself enforces (`409` and `403` respectively)."},"can_withdraw":{"type":"boolean","description":"True when the caller is the requester and `status` is still `pending`. Present on this shape as well as on the self view: an admin’s own request sits in their own queue, and withdrawing it is the one verb on that row that is theirs."}}}]}],"description":"Self view for the requester; admin view (`member:manage`) otherwise."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Visibility is resolved per caller, not gated by one fixed permission, the same shape as `GET /synthetic-tool/{id}`: an admin (`member:manage`) sees any request and gets the admin view; the request’s own requester sees it too, as the self view; anyone else gets `404` — a request’s existence is not for a bystander to learn.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Access request id (`areq_…`)."}]}},"/access-request/{id}/resolve":{"post":{"operationId":"resolveAccessRequest","summary":"Resolve someone else’s access request","tags":["Access requests"],"responses":{"200":{"description":"Resolved (admin view, `can_resolve: false`)","content":{"application/json":{"schema":{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction (`BlockedAppPayload.can_request_access`). Filing or resolving one never grants anything by itself — it is a record for a human to act on through the existing role/restriction APIs.","properties":{"id":{"type":"string","description":"Access request id (`areq_…`)."},"requester_user_id":{"type":"string","description":"User id (`usr_…`) of whoever filed the request."},"tool":{"type":"string","description":"The tool name the requester was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"],"description":"What kind of refusal this request is asking to be reconsidered."},"permission":{"type":["string","null"],"description":"Present only when `reason` is `\"permission\"` — never populated for a `\"restriction\"`-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact."},"note":{"type":["string","null"],"description":"Optional free text from the requester, ≤ 2000 characters."},"status":{"type":"string","enum":["pending","approved","denied","withdrawn"]},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by_user_id":{"type":["string","null"],"description":"User id (`usr_…`) of the admin who resolved it."},"resolution_note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","description":"The admin queue’s row shape — returned by `GET /access-request`, `GET /access-request/{id}` when the caller holds `member:manage`, and `POST /access-request/{id}/resolve`.","properties":{"requester":{"type":"object","description":"Resolved member profile. Falls back to `{ id }` alone when the profile row no longer resolves.","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"resolved_by":{"type":["object","null"],"description":"Resolved member profile. Falls back to `{ id }` alone when the profile row no longer resolves.","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"}}},"can_resolve":{"type":"boolean","description":"True while `status` is still `pending` **and** the row is not the caller’s own — the two preconditions `resolve` itself enforces (`409` and `403` respectively)."},"can_withdraw":{"type":"boolean","description":"True when the caller is the requester and `status` is still `pending`. Present on this shape as well as on the self view: an admin’s own request sits in their own queue, and withdrawing it is the one verb on that row that is theirs."}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"**This records a decision. It does not grant anything.** Approving here changes only this row’s own `status` and `resolution_note` — the requester’s role, restrictions and every permission they hold stay exactly as they were. Actually widening their access is a separate, explicit act through the existing role/restriction APIs (`PATCH /member/{userId}`, `PATCH /restriction/{id}`, …), which the admin still has to go make. Requires `member:manage`. Refuses with `409` when the request has already left `pending` — a decision is recorded once.\n\nA BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. A governance decision is recorded once and never re-opened, so the person recording it re-proves they are still the person holding the session. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it.\n\n**An admin may not decide their own request: `403`**, whichever decision they send and however many permissions they hold. Not because resolving could escalate anything — it cannot, per the paragraph above — but because the record is the product: \"approved by Roopi\" on Roopi’s own ask reads downstream exactly like an approval a second person signed. Enforced on the route, not only by `can_resolve`, so a client that ignores the capability field is refused rather than obeyed. The requesting admin keeps `can_withdraw` on that row instead.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decision"],"properties":{"decision":{"type":"string","enum":["approved","denied"]},"note":{"type":"string","maxLength":2000}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Access request id (`areq_…`)."}]}},"/access-request/{id}/withdraw":{"post":{"operationId":"withdrawAccessRequest","summary":"Withdraw an access request","tags":["Access requests"],"responses":{"200":{"description":"Withdrawn (self view, `can_withdraw: false`)","content":{"application/json":{"schema":{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction (`BlockedAppPayload.can_request_access`). Filing or resolving one never grants anything by itself — it is a record for a human to act on through the existing role/restriction APIs.","properties":{"id":{"type":"string","description":"Access request id (`areq_…`)."},"requester_user_id":{"type":"string","description":"User id (`usr_…`) of whoever filed the request."},"tool":{"type":"string","description":"The tool name the requester was blocked from calling."},"reason":{"type":"string","enum":["permission","restriction"],"description":"What kind of refusal this request is asking to be reconsidered."},"permission":{"type":["string","null"],"description":"Present only when `reason` is `\"permission\"` — never populated for a `\"restriction\"`-reason request, on either write or read. That is the disclosure rule: a restriction refusal never names the rule that blocked the caller, so this field must not become a second channel for the same fact."},"note":{"type":["string","null"],"description":"Optional free text from the requester, ≤ 2000 characters."},"status":{"type":"string","enum":["pending","approved","denied","withdrawn"]},"created_at":{"type":"string","format":"date-time"},"resolved_at":{"type":["string","null"],"format":"date-time"},"resolved_by_user_id":{"type":["string","null"],"description":"User id (`usr_…`) of the admin who resolved it."},"resolution_note":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","description":"The requester’s own view — returned by `POST /access-request`, `POST /access-request/{id}/withdraw`, and `GET /access-request/{id}` when the caller is the request’s own requester. `resolved_by_user_id` stays a bare id here: resolving it into a name/email is a member-directory lookup this view does not hand a plain member for free.","properties":{"can_withdraw":{"type":"boolean","description":"True when the caller is the requester and `status` is still `pending`."}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Only the request’s own requester may withdraw it, and only while it is still `pending`. No body, and no permission beyond membership — withdrawing your own request needs the same nothing that filing it did. Someone else’s request id is `404`; a request that already left `pending` is `409`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Access request id (`areq_…`)."}]}},"/observability-destination":{"get":{"operationId":"listObservabilityDestinations","summary":"List logging destinations","tags":["Logging destinations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","description":"Where the org audit trail is forwarded. Secrets are write-only and never appear in any response.","properties":{"id":{"type":"string","description":"Logging destination id (`ldst_…`)."},"type":{"type":"string","enum":["datadog","splunk_hec","sentinel"],"description":"Immutable after creation — to switch providers, create a new destination."},"name":{"type":"string"},"is_active":{"type":"boolean","description":"Paused destinations keep their config but forward nothing."},"config":{"type":"object","description":"Non-secret provider settings.","properties":{"site":{"type":"string","enum":["us1","us3","us5","eu","ap1"],"description":"Datadog site."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":200}},"log_types":{"type":"array","maxItems":10,"items":{"type":"string","enum":["tool_call","auth","admin"]},"description":"Which slices of the audit trail to forward: `tool_call` = MCP/tool executions, `auth` = membership, role, invite and credential events, `admin` = everything else."}}},"last_test_at":{"type":["string","null"],"format":"date-time"},"last_test_status":{"type":["string","null"],"enum":["ok","failed",null]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `logging:view` and the `logging_destinations` plan feature.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the destination's name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]},"post":{"operationId":"createObservabilityDestination","summary":"Create a logging destination","tags":["Logging destinations"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","description":"Where the org audit trail is forwarded. Secrets are write-only and never appear in any response.","properties":{"id":{"type":"string","description":"Logging destination id (`ldst_…`)."},"type":{"type":"string","enum":["datadog","splunk_hec","sentinel"],"description":"Immutable after creation — to switch providers, create a new destination."},"name":{"type":"string"},"is_active":{"type":"boolean","description":"Paused destinations keep their config but forward nothing."},"config":{"type":"object","description":"Non-secret provider settings.","properties":{"site":{"type":"string","enum":["us1","us3","us5","eu","ap1"],"description":"Datadog site."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":200}},"log_types":{"type":"array","maxItems":10,"items":{"type":"string","enum":["tool_call","auth","admin"]},"description":"Which slices of the audit trail to forward: `tool_call` = MCP/tool executions, `auth` = membership, role, invite and credential events, `admin` = everything else."}}},"last_test_at":{"type":["string","null"],"format":"date-time"},"last_test_status":{"type":["string","null"],"enum":["ok","failed",null]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `logging:manage` and the `logging_destinations` feature. For `datadog`, `config.site` and `secrets.api_key` are both required. Verify delivery with `POST /observability-destination/{id}/test` before relying on it.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","name"],"properties":{"type":{"type":"string","enum":["datadog","splunk_hec","sentinel"],"description":"Immutable after creation."},"name":{"type":"string","minLength":1,"maxLength":200},"is_active":{"type":"boolean"},"config":{"type":"object","description":"Non-secret provider settings.","properties":{"site":{"type":"string","enum":["us1","us3","us5","eu","ap1"],"description":"Datadog site."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":200}},"log_types":{"type":"array","maxItems":10,"items":{"type":"string","enum":["tool_call","auth","admin"]},"description":"Which slices of the audit trail to forward: `tool_call` = MCP/tool executions, `auth` = membership, role, invite and credential events, `admin` = everything else."}}},"secrets":{"type":"object","description":"Write-only credentials, stored encrypted and never returned. Sending `secrets` REPLACES the whole stored bag, so include every key the provider needs, not just the one you are changing.","properties":{"api_key":{"type":"string","description":"Datadog API key — required for `datadog`."},"token":{"type":"string","description":"Splunk HEC token."},"shared_key":{"type":"string","description":"Sentinel shared key."}}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/observability-destination/{id}":{"get":{"operationId":"getObservabilityDestination","summary":"Get a logging destination","tags":["Logging destinations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","description":"Where the org audit trail is forwarded. Secrets are write-only and never appear in any response.","properties":{"id":{"type":"string","description":"Logging destination id (`ldst_…`)."},"type":{"type":"string","enum":["datadog","splunk_hec","sentinel"],"description":"Immutable after creation — to switch providers, create a new destination."},"name":{"type":"string"},"is_active":{"type":"boolean","description":"Paused destinations keep their config but forward nothing."},"config":{"type":"object","description":"Non-secret provider settings.","properties":{"site":{"type":"string","enum":["us1","us3","us5","eu","ap1"],"description":"Datadog site."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":200}},"log_types":{"type":"array","maxItems":10,"items":{"type":"string","enum":["tool_call","auth","admin"]},"description":"Which slices of the audit trail to forward: `tool_call` = MCP/tool executions, `auth` = membership, role, invite and credential events, `admin` = everything else."}}},"last_test_at":{"type":["string","null"],"format":"date-time"},"last_test_status":{"type":["string","null"],"enum":["ok","failed",null]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `logging:view`. Secrets are never included.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Logging destination id (`ldst_…`)."}]},"patch":{"operationId":"updateObservabilityDestination","summary":"Update a logging destination","tags":["Logging destinations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","description":"Where the org audit trail is forwarded. Secrets are write-only and never appear in any response.","properties":{"id":{"type":"string","description":"Logging destination id (`ldst_…`)."},"type":{"type":"string","enum":["datadog","splunk_hec","sentinel"],"description":"Immutable after creation — to switch providers, create a new destination."},"name":{"type":"string"},"is_active":{"type":"boolean","description":"Paused destinations keep their config but forward nothing."},"config":{"type":"object","description":"Non-secret provider settings.","properties":{"site":{"type":"string","enum":["us1","us3","us5","eu","ap1"],"description":"Datadog site."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":200}},"log_types":{"type":"array","maxItems":10,"items":{"type":"string","enum":["tool_call","auth","admin"]},"description":"Which slices of the audit trail to forward: `tool_call` = MCP/tool executions, `auth` = membership, role, invite and credential events, `admin` = everything else."}}},"last_test_at":{"type":["string","null"],"format":"date-time"},"last_test_status":{"type":["string","null"],"enum":["ok","failed",null]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"`config` is shallow-MERGED over the stored config, so you may send just the keys you are changing. **`secrets`, in contrast, REPLACES the stored bag entirely** — send every credential the provider needs, or omit `secrets` to leave the existing ones alone. `type` cannot be changed. Requires `logging:manage`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"is_active":{"type":"boolean"},"config":{"type":"object","description":"Non-secret provider settings.","properties":{"site":{"type":"string","enum":["us1","us3","us5","eu","ap1"],"description":"Datadog site."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":200}},"log_types":{"type":"array","maxItems":10,"items":{"type":"string","enum":["tool_call","auth","admin"]},"description":"Which slices of the audit trail to forward: `tool_call` = MCP/tool executions, `auth` = membership, role, invite and credential events, `admin` = everything else."}}},"secrets":{"type":"object","description":"Write-only credentials, stored encrypted and never returned. Sending `secrets` REPLACES the whole stored bag, so include every key the provider needs, not just the one you are changing.","properties":{"api_key":{"type":"string","description":"Datadog API key — required for `datadog`."},"token":{"type":"string","description":"Splunk HEC token."},"shared_key":{"type":"string","description":"Sentinel shared key."}}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Logging destination id (`ldst_…`)."}]},"delete":{"operationId":"deleteObservabilityDestination","summary":"Delete a logging destination","tags":["Logging destinations"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible, including the stored credentials — forwarding stops at once. To pause instead, PATCH `is_active: false`. The audit log itself is unaffected. Requires `logging:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Logging destination id (`ldst_…`)."}]}},"/observability-destination/{id}/test":{"post":{"operationId":"testObservabilityDestination","summary":"Send a test event to a logging destination","tags":["Logging destinations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"status":{"type":["integer","null"],"description":"HTTP status returned by the provider, when there was one."},"error":{"type":["string","null"],"description":"Failure detail; null when `ok` is true."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Really delivers one synthetic admin event through the live forwarder and reports the outcome, updating `last_test_at`/`last_test_status`. Works on paused destinations. Optional `config`/`secrets` in the body are merged over the stored ones for this call only — nothing is saved — so edits can be verified before being written. Requires `logging:manage`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","description":"Non-secret provider settings.","properties":{"site":{"type":"string","enum":["us1","us3","us5","eu","ap1"],"description":"Datadog site."},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":200}},"log_types":{"type":"array","maxItems":10,"items":{"type":"string","enum":["tool_call","auth","admin"]},"description":"Which slices of the audit trail to forward: `tool_call` = MCP/tool executions, `auth` = membership, role, invite and credential events, `admin` = everything else."}}},"secrets":{"type":"object","description":"Write-only credentials, stored encrypted and never returned. Sending `secrets` REPLACES the whole stored bag, so include every key the provider needs, not just the one you are changing.","properties":{"api_key":{"type":"string","description":"Datadog API key — required for `datadog`."},"token":{"type":"string","description":"Splunk HEC token."},"shared_key":{"type":"string","description":"Sentinel shared key."}}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Logging destination id (`ldst_…`)."}]}},"/notification-destination":{"get":{"operationId":"listOrgNotificationDestinations","summary":"List notification destinations","tags":["Notification destinations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","description":"Where org notification events are sent. The Slack webhook URL is write-only and never appears in any response.","properties":{"id":{"type":"string","description":"Notification destination id (`onfd_…`)."},"type":{"type":"string","enum":["slack","email"],"description":"Immutable after creation."},"name":{"type":"string"},"is_active":{"type":"boolean","description":"Paused destinations keep their config but forward nothing."},"config":{"type":"object","description":"Non-secret settings. `to`/`cc`/`bcc` apply to `email`; ignored for `slack`.","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject_prefix":{"type":"string","maxLength":50}}},"event_types":{"type":"array","items":{"type":"string","enum":["connection.needs_reauth","connection.disconnected","member.joined","member.removed","member.roles_updated","invite.sent","invite.revoked","role.created","role.updated","role.deleted","restriction.updated","connector.created","connector.destination_changed","api_token.created","api_token.revoked","sso_connection.created","sso_connection.updated","mfa.disabled","team.member_added","team.member_removed"]},"description":"Fixed catalog of org events this destination is subscribed to."},"last_test_at":{"type":["string","null"],"format":"date-time"},"last_test_status":{"type":["string","null"],"enum":["ok","failed",null]},"created_by_user_id":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `notification:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the destination's name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]},"post":{"operationId":"createOrgNotificationDestination","summary":"Create a notification destination","tags":["Notification destinations"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","description":"Where org notification events are sent. The Slack webhook URL is write-only and never appears in any response.","properties":{"id":{"type":"string","description":"Notification destination id (`onfd_…`)."},"type":{"type":"string","enum":["slack","email"],"description":"Immutable after creation."},"name":{"type":"string"},"is_active":{"type":"boolean","description":"Paused destinations keep their config but forward nothing."},"config":{"type":"object","description":"Non-secret settings. `to`/`cc`/`bcc` apply to `email`; ignored for `slack`.","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject_prefix":{"type":"string","maxLength":50}}},"event_types":{"type":"array","items":{"type":"string","enum":["connection.needs_reauth","connection.disconnected","member.joined","member.removed","member.roles_updated","invite.sent","invite.revoked","role.created","role.updated","role.deleted","restriction.updated","connector.created","connector.destination_changed","api_token.created","api_token.revoked","sso_connection.created","sso_connection.updated","mfa.disabled","team.member_added","team.member_removed"]},"description":"Fixed catalog of org events this destination is subscribed to."},"last_test_at":{"type":["string","null"],"format":"date-time"},"last_test_status":{"type":["string","null"],"enum":["ok","failed",null]},"created_by_user_id":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `notification:manage`. `slack` requires `webhook_url`; `email` requires `config.to`. Verify delivery with `POST /notification-destination/{id}/test` before relying on it.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["type","name"],"properties":{"type":{"type":"string","enum":["slack","email"],"description":"Immutable after creation."},"name":{"type":"string","minLength":1,"maxLength":200},"is_active":{"type":"boolean"},"config":{"type":"object","description":"Non-secret settings. `to`/`cc`/`bcc` apply to `email`; ignored for `slack`.","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject_prefix":{"type":"string","maxLength":50}}},"webhook_url":{"type":"string","description":"Slack incoming-webhook URL. Ignored for `type: \"email\"`."},"event_types":{"type":"array","items":{"type":"string","enum":["connection.needs_reauth","connection.disconnected","member.joined","member.removed","member.roles_updated","invite.sent","invite.revoked","role.created","role.updated","role.deleted","restriction.updated","connector.created","connector.destination_changed","api_token.created","api_token.revoked","sso_connection.created","sso_connection.updated","mfa.disabled","team.member_added","team.member_removed"]},"default":[]}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/notification-destination/{id}":{"get":{"operationId":"getOrgNotificationDestination","summary":"Get a notification destination","tags":["Notification destinations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","description":"Where org notification events are sent. The Slack webhook URL is write-only and never appears in any response.","properties":{"id":{"type":"string","description":"Notification destination id (`onfd_…`)."},"type":{"type":"string","enum":["slack","email"],"description":"Immutable after creation."},"name":{"type":"string"},"is_active":{"type":"boolean","description":"Paused destinations keep their config but forward nothing."},"config":{"type":"object","description":"Non-secret settings. `to`/`cc`/`bcc` apply to `email`; ignored for `slack`.","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject_prefix":{"type":"string","maxLength":50}}},"event_types":{"type":"array","items":{"type":"string","enum":["connection.needs_reauth","connection.disconnected","member.joined","member.removed","member.roles_updated","invite.sent","invite.revoked","role.created","role.updated","role.deleted","restriction.updated","connector.created","connector.destination_changed","api_token.created","api_token.revoked","sso_connection.created","sso_connection.updated","mfa.disabled","team.member_added","team.member_removed"]},"description":"Fixed catalog of org events this destination is subscribed to."},"last_test_at":{"type":["string","null"],"format":"date-time"},"last_test_status":{"type":["string","null"],"enum":["ok","failed",null]},"created_by_user_id":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `notification:view`. Secrets are never included.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Notification destination id (`onfd_…`)."}]},"patch":{"operationId":"updateOrgNotificationDestination","summary":"Update a notification destination","tags":["Notification destinations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","description":"Where org notification events are sent. The Slack webhook URL is write-only and never appears in any response.","properties":{"id":{"type":"string","description":"Notification destination id (`onfd_…`)."},"type":{"type":"string","enum":["slack","email"],"description":"Immutable after creation."},"name":{"type":"string"},"is_active":{"type":"boolean","description":"Paused destinations keep their config but forward nothing."},"config":{"type":"object","description":"Non-secret settings. `to`/`cc`/`bcc` apply to `email`; ignored for `slack`.","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject_prefix":{"type":"string","maxLength":50}}},"event_types":{"type":"array","items":{"type":"string","enum":["connection.needs_reauth","connection.disconnected","member.joined","member.removed","member.roles_updated","invite.sent","invite.revoked","role.created","role.updated","role.deleted","restriction.updated","connector.created","connector.destination_changed","api_token.created","api_token.revoked","sso_connection.created","sso_connection.updated","mfa.disabled","team.member_added","team.member_removed"]},"description":"Fixed catalog of org events this destination is subscribed to."},"last_test_at":{"type":["string","null"],"format":"date-time"},"last_test_status":{"type":["string","null"],"enum":["ok","failed",null]},"created_by_user_id":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"`config` is shallow-MERGED over the stored config. **`webhook_url`, when present, REPLACES the stored secret entirely.** `type` cannot be changed. Requires `notification:manage`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"is_active":{"type":"boolean"},"config":{"type":"object","description":"Non-secret settings. `to`/`cc`/`bcc` apply to `email`; ignored for `slack`.","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject_prefix":{"type":"string","maxLength":50}}},"webhook_url":{"type":["string","null"]},"event_types":{"type":"array","items":{"type":"string","enum":["connection.needs_reauth","connection.disconnected","member.joined","member.removed","member.roles_updated","invite.sent","invite.revoked","role.created","role.updated","role.deleted","restriction.updated","connector.created","connector.destination_changed","api_token.created","api_token.revoked","sso_connection.created","sso_connection.updated","mfa.disabled","team.member_added","team.member_removed"]}}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Notification destination id (`onfd_…`)."}]},"delete":{"operationId":"deleteOrgNotificationDestination","summary":"Delete a notification destination","tags":["Notification destinations"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible, including the stored webhook credential — forwarding stops at once. To pause instead, PATCH `is_active: false`. Requires `notification:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Notification destination id (`onfd_…`)."}]}},"/notification-destination/{id}/test":{"post":{"operationId":"testOrgNotificationDestination","summary":"Send a test event to a notification destination","tags":["Notification destinations"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"error":{"type":["string","null"],"description":"Failure detail; null when `ok` is true."}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Really delivers one synthetic event through the live Slack/email path and reports the outcome, updating `last_test_at`/`last_test_status`. Works on paused destinations. Optional `config`/`webhook_url` in the body are used for this call only — nothing is saved — so edits can be verified before being written. Requires `notification:manage`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","description":"Non-secret settings. `to`/`cc`/`bcc` apply to `email`; ignored for `slack`.","properties":{"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject_prefix":{"type":"string","maxLength":50}}},"webhook_url":{"type":"string"}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Notification destination id (`onfd_…`)."}]}},"/sso-connection":{"get":{"operationId":"listSsoConnections","summary":"List SSO connections","tags":["SSO connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"SSO connection id (`sso_…`)."},"organization_id":{"type":"string","description":"Owning organization (`org_…`)."},"protocol":{"type":"string","enum":["oidc","saml"]},"name":{"type":"string"},"is_active":{"type":"boolean","description":"A draft may be saved incomplete, but cannot be activated until the protocol's required fields are present."},"enforced":{"type":"boolean","description":"When enforced, users on the connection's verified domains can ONLY sign in through this IdP — social login and email codes are refused for them."},"is_default":{"type":"boolean","description":"The connection domain resolution picks when a domain has several."},"config":{"type":"object","description":"Protocol-specific IdP settings. Never contains secrets — the OIDC client secret is write-only.","properties":{"idp_entity_id":{"type":"string","description":"SAML: IdP entity id."},"idp_sso_url":{"type":"string","description":"SAML: IdP single sign-on URL."},"idp_x509_cert":{"type":"string","description":"SAML: IdP signing certificate (PEM)."},"idp_metadata_xml":{"type":"string","description":"SAML: paste IdP metadata to populate the fields above. Accepted on write; never returned."},"issuer":{"type":"string","description":"OIDC: issuer URL."},"client_id":{"type":"string","description":"OIDC: relying-party client id."},"discovery_url":{"type":"string","description":"OIDC: discovery document URL."},"sign_authn_requests":{"type":"boolean","description":"SAML: sign SP-initiated AuthnRequests."},"allow_idp_initiated":{"type":"boolean","description":"SAML: accept unsolicited IdP-initiated responses. Rejected by default."}}},"default_role_id":{"type":["string","null"],"description":"Role (`role_…`) explicitly configured for users provisioned through this connection, or null to track the default. Org Owner is refused here. Read `effective_default_role_id` to learn what a JIT login actually grants."},"effective_default_role_id":{"type":["string","null"],"description":"Server-computed: the role SSO just-in-time provisioning through this connection really grants. Equals `default_role_id` when that is set and the role still exists; otherwise the **Member** system role, which is what the join falls back to both when nothing is configured and when the configured role has since been deleted. `null` means even that fallback is missing, so auto-provisioning through this connection is broken and the first login seats nobody."},"has_client_secret":{"type":"boolean","description":"Whether an OIDC client secret is stored. The secret itself is never returned."},"urls":{"type":"object","additionalProperties":true,"description":"The protocol URLs to hand to the IdP administrator (ACS / metadata / callback, by protocol)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `sso:view` and the `sso` plan feature.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"post":{"operationId":"createSsoConnection","summary":"Create an SSO connection","tags":["SSO connections"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"SSO connection id (`sso_…`)."},"organization_id":{"type":"string","description":"Owning organization (`org_…`)."},"protocol":{"type":"string","enum":["oidc","saml"]},"name":{"type":"string"},"is_active":{"type":"boolean","description":"A draft may be saved incomplete, but cannot be activated until the protocol's required fields are present."},"enforced":{"type":"boolean","description":"When enforced, users on the connection's verified domains can ONLY sign in through this IdP — social login and email codes are refused for them."},"is_default":{"type":"boolean","description":"The connection domain resolution picks when a domain has several."},"config":{"type":"object","description":"Protocol-specific IdP settings. Never contains secrets — the OIDC client secret is write-only.","properties":{"idp_entity_id":{"type":"string","description":"SAML: IdP entity id."},"idp_sso_url":{"type":"string","description":"SAML: IdP single sign-on URL."},"idp_x509_cert":{"type":"string","description":"SAML: IdP signing certificate (PEM)."},"idp_metadata_xml":{"type":"string","description":"SAML: paste IdP metadata to populate the fields above. Accepted on write; never returned."},"issuer":{"type":"string","description":"OIDC: issuer URL."},"client_id":{"type":"string","description":"OIDC: relying-party client id."},"discovery_url":{"type":"string","description":"OIDC: discovery document URL."},"sign_authn_requests":{"type":"boolean","description":"SAML: sign SP-initiated AuthnRequests."},"allow_idp_initiated":{"type":"boolean","description":"SAML: accept unsolicited IdP-initiated responses. Rejected by default."}}},"default_role_id":{"type":["string","null"],"description":"Role (`role_…`) explicitly configured for users provisioned through this connection, or null to track the default. Org Owner is refused here. Read `effective_default_role_id` to learn what a JIT login actually grants."},"effective_default_role_id":{"type":["string","null"],"description":"Server-computed: the role SSO just-in-time provisioning through this connection really grants. Equals `default_role_id` when that is set and the role still exists; otherwise the **Member** system role, which is what the join falls back to both when nothing is configured and when the configured role has since been deleted. `null` means even that fallback is missing, so auto-provisioning through this connection is broken and the first login seats nobody."},"has_client_secret":{"type":"boolean","description":"Whether an OIDC client secret is stored. The secret itself is never returned."},"urls":{"type":"object","additionalProperties":true,"description":"The protocol URLs to hand to the IdP administrator (ACS / metadata / callback, by protocol)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `sso:manage` and the `sso` feature. Create it inactive, hand the returned `urls` to the IdP administrator, then activate it with PATCH — activation is refused while the protocol's required config is incomplete. Domains are attached separately, by pointing an org domain at this connection with `PATCH /org-domain/{id}`. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. It is gated because a verified domain and an SSO connection are DEFERRED membership grants: they admit a principal with no actor present at the moment they fire, which is `POST /invite` with a longer fuse.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","protocol"],"properties":{"protocol":{"type":"string","enum":["oidc","saml"],"description":"Immutable after creation."},"name":{"type":"string","minLength":1,"maxLength":200},"is_active":{"type":"boolean","default":false},"enforced":{"type":"boolean","default":false,"description":"See `enforced` on the response — this locks users to the IdP."},"is_default":{"type":"boolean","default":false},"config":{"type":"object","description":"Protocol-specific IdP settings. Never contains secrets — the OIDC client secret is write-only.","properties":{"idp_entity_id":{"type":"string","description":"SAML: IdP entity id."},"idp_sso_url":{"type":"string","description":"SAML: IdP single sign-on URL."},"idp_x509_cert":{"type":"string","description":"SAML: IdP signing certificate (PEM)."},"idp_metadata_xml":{"type":"string","description":"SAML: paste IdP metadata to populate the fields above. Accepted on write; never returned."},"issuer":{"type":"string","description":"OIDC: issuer URL."},"client_id":{"type":"string","description":"OIDC: relying-party client id."},"discovery_url":{"type":"string","description":"OIDC: discovery document URL."},"sign_authn_requests":{"type":"boolean","description":"SAML: sign SP-initiated AuthnRequests."},"allow_idp_initiated":{"type":"boolean","description":"SAML: accept unsolicited IdP-initiated responses. Rejected by default."}}},"client_secret":{"type":"string","description":"OIDC client secret. Write-only — stored encrypted, never returned."},"default_role_id":{"type":["string","null"],"description":"Role id (`role_…`)."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/sso-connection/{id}":{"get":{"operationId":"getSsoConnection","summary":"Get an SSO connection","tags":["SSO connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"SSO connection id (`sso_…`)."},"organization_id":{"type":"string","description":"Owning organization (`org_…`)."},"protocol":{"type":"string","enum":["oidc","saml"]},"name":{"type":"string"},"is_active":{"type":"boolean","description":"A draft may be saved incomplete, but cannot be activated until the protocol's required fields are present."},"enforced":{"type":"boolean","description":"When enforced, users on the connection's verified domains can ONLY sign in through this IdP — social login and email codes are refused for them."},"is_default":{"type":"boolean","description":"The connection domain resolution picks when a domain has several."},"config":{"type":"object","description":"Protocol-specific IdP settings. Never contains secrets — the OIDC client secret is write-only.","properties":{"idp_entity_id":{"type":"string","description":"SAML: IdP entity id."},"idp_sso_url":{"type":"string","description":"SAML: IdP single sign-on URL."},"idp_x509_cert":{"type":"string","description":"SAML: IdP signing certificate (PEM)."},"idp_metadata_xml":{"type":"string","description":"SAML: paste IdP metadata to populate the fields above. Accepted on write; never returned."},"issuer":{"type":"string","description":"OIDC: issuer URL."},"client_id":{"type":"string","description":"OIDC: relying-party client id."},"discovery_url":{"type":"string","description":"OIDC: discovery document URL."},"sign_authn_requests":{"type":"boolean","description":"SAML: sign SP-initiated AuthnRequests."},"allow_idp_initiated":{"type":"boolean","description":"SAML: accept unsolicited IdP-initiated responses. Rejected by default."}}},"default_role_id":{"type":["string","null"],"description":"Role (`role_…`) explicitly configured for users provisioned through this connection, or null to track the default. Org Owner is refused here. Read `effective_default_role_id` to learn what a JIT login actually grants."},"effective_default_role_id":{"type":["string","null"],"description":"Server-computed: the role SSO just-in-time provisioning through this connection really grants. Equals `default_role_id` when that is set and the role still exists; otherwise the **Member** system role, which is what the join falls back to both when nothing is configured and when the configured role has since been deleted. `null` means even that fallback is missing, so auto-provisioning through this connection is broken and the first login seats nobody."},"has_client_secret":{"type":"boolean","description":"Whether an OIDC client secret is stored. The secret itself is never returned."},"urls":{"type":"object","additionalProperties":true,"description":"The protocol URLs to hand to the IdP administrator (ACS / metadata / callback, by protocol)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `sso:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SSO connection id (`sso_…`)."}]},"patch":{"operationId":"updateSsoConnection","summary":"Update an SSO connection","tags":["SSO connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"SSO connection id (`sso_…`)."},"organization_id":{"type":"string","description":"Owning organization (`org_…`)."},"protocol":{"type":"string","enum":["oidc","saml"]},"name":{"type":"string"},"is_active":{"type":"boolean","description":"A draft may be saved incomplete, but cannot be activated until the protocol's required fields are present."},"enforced":{"type":"boolean","description":"When enforced, users on the connection's verified domains can ONLY sign in through this IdP — social login and email codes are refused for them."},"is_default":{"type":"boolean","description":"The connection domain resolution picks when a domain has several."},"config":{"type":"object","description":"Protocol-specific IdP settings. Never contains secrets — the OIDC client secret is write-only.","properties":{"idp_entity_id":{"type":"string","description":"SAML: IdP entity id."},"idp_sso_url":{"type":"string","description":"SAML: IdP single sign-on URL."},"idp_x509_cert":{"type":"string","description":"SAML: IdP signing certificate (PEM)."},"idp_metadata_xml":{"type":"string","description":"SAML: paste IdP metadata to populate the fields above. Accepted on write; never returned."},"issuer":{"type":"string","description":"OIDC: issuer URL."},"client_id":{"type":"string","description":"OIDC: relying-party client id."},"discovery_url":{"type":"string","description":"OIDC: discovery document URL."},"sign_authn_requests":{"type":"boolean","description":"SAML: sign SP-initiated AuthnRequests."},"allow_idp_initiated":{"type":"boolean","description":"SAML: accept unsolicited IdP-initiated responses. Rejected by default."}}},"default_role_id":{"type":["string","null"],"description":"Role (`role_…`) explicitly configured for users provisioned through this connection, or null to track the default. Org Owner is refused here. Read `effective_default_role_id` to learn what a JIT login actually grants."},"effective_default_role_id":{"type":["string","null"],"description":"Server-computed: the role SSO just-in-time provisioning through this connection really grants. Equals `default_role_id` when that is set and the role still exists; otherwise the **Member** system role, which is what the join falls back to both when nothing is configured and when the configured role has since been deleted. `null` means even that fallback is missing, so auto-provisioning through this connection is broken and the first login seats nobody."},"has_client_secret":{"type":"boolean","description":"Whether an OIDC client secret is stored. The secret itself is never returned."},"urls":{"type":"object","additionalProperties":true,"description":"The protocol URLs to hand to the IdP administrator (ACS / metadata / callback, by protocol)."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"`config` is shallow-MERGED over the stored config, so partial updates are safe. `client_secret` replaces the stored secret; an empty string clears it. `protocol` cannot be changed. Setting `is_active: true` (or editing a live connection) re-checks that the required IdP fields are present. Turning on `enforced` immediately blocks non-IdP sign-in for that domain's users — verify the connection works first. Requires `sso:manage`. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. It is gated because a verified domain and an SSO connection are DEFERRED membership grants: they admit a principal with no actor present at the moment they fire, which is `POST /invite` with a longer fuse.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"is_active":{"type":"boolean"},"enforced":{"type":"boolean"},"is_default":{"type":"boolean"},"config":{"type":"object","description":"Protocol-specific IdP settings. Never contains secrets — the OIDC client secret is write-only.","properties":{"idp_entity_id":{"type":"string","description":"SAML: IdP entity id."},"idp_sso_url":{"type":"string","description":"SAML: IdP single sign-on URL."},"idp_x509_cert":{"type":"string","description":"SAML: IdP signing certificate (PEM)."},"idp_metadata_xml":{"type":"string","description":"SAML: paste IdP metadata to populate the fields above. Accepted on write; never returned."},"issuer":{"type":"string","description":"OIDC: issuer URL."},"client_id":{"type":"string","description":"OIDC: relying-party client id."},"discovery_url":{"type":"string","description":"OIDC: discovery document URL."},"sign_authn_requests":{"type":"boolean","description":"SAML: sign SP-initiated AuthnRequests."},"allow_idp_initiated":{"type":"boolean","description":"SAML: accept unsolicited IdP-initiated responses. Rejected by default."}}},"client_secret":{"type":"string","description":"Replaces the stored secret; `\"\"` clears it."},"default_role_id":{"type":["string","null"]}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SSO connection id (`sso_…`)."}]},"delete":{"operationId":"deleteSsoConnection","summary":"Delete an SSO connection","tags":["SSO connections"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible, including the stored secrets. Org domains pointing at it are unlinked, so their users fall back to the other login methods — but users who have no other credential will be locked out until another route is configured. Existing sessions are not revoked. Requires `sso:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SSO connection id (`sso_…`)."}]}},"/scim-token":{"get":{"operationId":"listScimTokens","summary":"List SCIM tokens","tags":["SCIM tokens"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","description":"A bearer token the IdP uses for `/scim/v2/*` only. It cannot call the rest of this API.","properties":{"id":{"type":"string","description":"SCIM token id."},"organization_id":{"type":"string","description":"Organization (`org_…`) it provisions into."},"user_id":{"type":"string","description":"User (`usr_…`) who minted it."},"name":{"type":"string"},"last_used_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Metadata only. Requires `sso:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"post":{"operationId":"createScimToken","summary":"Create a SCIM token","tags":["SCIM tokens"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"allOf":[{"type":"object","description":"A bearer token the IdP uses for `/scim/v2/*` only. It cannot call the rest of this API.","properties":{"id":{"type":"string","description":"SCIM token id."},"organization_id":{"type":"string","description":"Organization (`org_…`) it provisions into."},"user_id":{"type":"string","description":"User (`usr_…`) who minted it."},"name":{"type":"string"},"last_used_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},{"type":"object","properties":{"token":{"type":"string","description":"Raw SCIM token — shown once."}}}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Mints the bearer token an IdP uses to provision users and groups through `/scim/v2/*`. The raw `token` (`escim_…`) is in THIS response only and cannot be retrieved again. It authorizes SCIM alone — it is not an org API token. Requires `sso:manage`, an interactive human session (a credential cannot mint one), and a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`) — this bearer carries full provisioning rights over the directory and outlives whatever minted it, so minting one is what a stolen session would otherwise do to walk around every step-up gate. Without the token the call answers `428 step_up_required`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/scim-token/{id}":{"delete":{"operationId":"deleteScimToken","summary":"Revoke a SCIM token","tags":["SCIM tokens"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Irreversible and immediate: the IdP stops being able to provision, and directory changes silently stop flowing until a new token is configured there. Existing members keep their access. Requires `sso:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM token id — not the token itself."}]}},"/scim-group":{"get":{"operationId":"listScimGroups","summary":"List SCIM groups","tags":["SCIM groups"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","additionalProperties":true}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Read-only view of the groups the IdP has pushed, for populating the group → role mapping picker. Groups themselves are created and deleted by the IdP through `/scim/v2/Groups`, never here. Each row carries a bounded membership summary — `member_count` plus `member_preview` (at most 5 display names) — never the membership itself; page that through `GET /scim-group/{id}/member`. Cursor-paginated. Requires `sso:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on the group's display name. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]}},"/scim-group/{id}/member":{"get":{"operationId":"listScimGroupMembers","summary":"List a SCIM group’s members","tags":["SCIM groups"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string","description":"Member user id (`usr_…`)."},"display_name":{"type":"string"},"email":{"type":["string","null"]}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The membership behind a group row’s `member_count`. `display_name` is the member’s name, else email, else the bare `usr_…` id when the IdP pushed a member this organization cannot resolve — for those rows `email` is `null`, and `q` (which searches names and emails) cannot match them. Unknown group → `404`. Cursor-paginated. Requires `sso:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM group id (`sgrp_…`)."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match over member display names and emails. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}}]}},"/group-mapping":{"get":{"operationId":"listGroupMappings","summary":"List SCIM group → role mappings","tags":["Group mapping"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Group mapping id (`gmap_…`)."},"scim_group_id":{"type":"string","description":"SCIM group from `GET /scim-group`."},"role_ids":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The single role (`role_…`) every member of that group receives."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Cursor-paginated. Requires `sso:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]},"post":{"operationId":"createGroupMapping","summary":"Create a group → role mapping","tags":["Group mapping"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Group mapping id (`gmap_…`)."},"scim_group_id":{"type":"string","description":"SCIM group from `GET /scim-group`."},"role_ids":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The single role (`role_…`) every member of that group receives."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Every member of the SCIM group receives these roles. The model is a UNION: mapped roles merge onto roles assigned manually through `PATCH /member/{userId}`, they do not replace them. Roles are recomputed for affected members immediately, and again on every SCIM membership change. Requires `sso:manage`. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it. It is gated because a verified domain and an SSO connection are DEFERRED membership grants: they admit a principal with no actor present at the moment they fire, which is `POST /invite` with a longer fuse.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scim_group_id"],"properties":{"scim_group_id":{"type":"string","description":"Group id from `GET /scim-group`."},"role_ids":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"At most one role (`role_…`). Defaults to empty (no mapping)."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/group-mapping/{id}":{"get":{"operationId":"getGroupMapping","summary":"Get a group mapping","tags":["Group mapping"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Group mapping id (`gmap_…`)."},"scim_group_id":{"type":"string","description":"SCIM group from `GET /scim-group`."},"role_ids":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The single role (`role_…`) every member of that group receives."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Requires `sso:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Group mapping id (`gmap_…`)."}]},"patch":{"operationId":"updateGroupMapping","summary":"Update a group mapping","tags":["Group mapping"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Group mapping id (`gmap_…`)."},"scim_group_id":{"type":"string","description":"SCIM group from `GET /scim-group`."},"role_ids":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"The single role (`role_…`) every member of that group receives."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"**`role_ids` REPLACES the mapped role when present** — at most one role, since a member holds exactly one (two or more answers 400 `role_cardinality`). Members lose the old mapped role on their next recompute; manually assigned roles are untouched and always win. Requires `sso:manage`, and — because a mapping is a deferred grant applied with no actor present — you may only map a role whose permissions you hold yourself. A BROWSER SESSION must additionally carry a fresh step-up reauthentication (`X-Step-Up-Token`, obtained from `/auth/step-up`); without one the call answers `428 step_up_required` and `error.details` names the action and resource to prove. An organization API token is not challenged: step-up re-proves the person behind a session, and a token has no person behind it.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scim_group_id":{"type":"string"},"role_ids":{"type":"array","maxItems":1,"items":{"type":"string"},"description":"Replaces the mapped role; at most one."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Group mapping id (`gmap_…`)."}]},"delete":{"operationId":"deleteGroupMapping","summary":"Delete a group mapping","tags":["Group mapping"],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Members of that group lose the mapped roles — and with them the permissions those carried — as soon as roles are recomputed. Manually assigned roles survive. Requires `sso:manage`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Group mapping id (`gmap_…`)."}]}},"/scim/v2/ServiceProviderConfig":{"get":{"operationId":"scimServiceProviderConfig","summary":"SCIM service provider configuration","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Advertises which SCIM features this server supports (PATCH yes, bulk no, filter yes).","security":[{"bearerAuth":[]}]}},"/scim/v2/ResourceTypes":{"get":{"operationId":"scimResourceTypes","summary":"SCIM resource types","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The resource types served here: `User` and `Group`.","security":[{"bearerAuth":[]}]}},"/scim/v2/Schemas":{"get":{"operationId":"scimSchemas","summary":"SCIM schemas","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The core User and Group schemas this server implements.","security":[{"bearerAuth":[]}]}},"/scim/v2/Users":{"get":{"operationId":"scimListUsers","summary":"SCIM List Users","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"The SCIM data plane, authenticated by a SCIM bearer token (`POST /scim-token`) — not a session and not an org API token; the organization comes from the token, so no `X-Organization-Id`. Paginates with SCIM's `startIndex`/`count`, not this API's cursors.","security":[{"bearerAuth":[]}],"parameters":[{"name":"filter","in":"query","required":false,"description":"SCIM filter expression, e.g. `userName eq \"a@b.com\"`.","schema":{"type":"string"}},{"name":"startIndex","in":"query","required":false,"description":"1-based index of the first result (SCIM pagination, not this API's cursors).","schema":{"type":"integer","default":1}},{"name":"count","in":"query","required":false,"description":"Page size. Defaults to 100; the service advertises a maximum of 200.","schema":{"type":"integer","default":100}}]},"post":{"operationId":"scimCreateUser","summary":"SCIM Create User","tags":["SCIM v2"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Provisions a member into the token's organization. Called by the IdP.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}}}},"/scim/v2/Users/{id}":{"get":{"operationId":"scimGetUser","summary":"SCIM Get User","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM user id."}]},"put":{"operationId":"scimReplaceUser","summary":"SCIM Replace User","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"REPLACES the resource with the body — SCIM PUT semantics. Use PATCH for partial changes.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM user id."}]},"patch":{"operationId":"scimPatchUser","summary":"SCIM Patch User","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"SCIM PATCH operations (`add`/`replace`/`remove`) — this is how an IdP deactivates a user.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM user id."}]},"delete":{"operationId":"scimDeleteUser","summary":"SCIM Delete User","tags":["SCIM v2"],"responses":{"204":{"description":"No content."},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Deprovisions the member — the organization-access consequences of `DELETE /member/{userId}` apply.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM user id."}]}},"/scim/v2/Groups":{"get":{"operationId":"scimListGroups","summary":"SCIM List Groups","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"parameters":[{"name":"filter","in":"query","required":false,"description":"SCIM filter expression, e.g. `userName eq \"a@b.com\"`.","schema":{"type":"string"}},{"name":"startIndex","in":"query","required":false,"description":"1-based index of the first result (SCIM pagination, not this API's cursors).","schema":{"type":"integer","default":1}},{"name":"count","in":"query","required":false,"description":"Page size. Defaults to 100; the service advertises a maximum of 200.","schema":{"type":"integer","default":100}}]},"post":{"operationId":"scimCreateGroup","summary":"SCIM Create Group","tags":["SCIM v2"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Groups created here are what `GET /scim-group` lists and what group mappings point at.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}}}},"/scim/v2/Groups/{id}":{"get":{"operationId":"scimGetGroup","summary":"SCIM Get Group","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM group id."}]},"put":{"operationId":"scimReplaceGroup","summary":"SCIM Replace Group","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"REPLACES the group, membership included. Membership changes recompute mapped roles for everyone affected.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM group id."}]},"patch":{"operationId":"scimPatchGroup","summary":"SCIM Patch Group","tags":["SCIM v2"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"SCIM PATCH operations, typically adding or removing members.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"description":"SCIM 2.0 resource or ListResponse, served as `application/scim+json`."}}}},"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM group id."}]},"delete":{"operationId":"scimDeleteGroup","summary":"SCIM Delete Group","tags":["SCIM v2"],"responses":{"204":{"description":"No content."},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Members lose any roles granted through a mapping onto this group; manually assigned roles are unaffected.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM group id."}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"session or API token","description":"Session cookie `elaichi_session` or `Authorization: Bearer <token>` (session token or org-scoped API token `elch_{org_id}_{64 hex}`). Two other credential families exist and are NOT interchangeable with this one: an MCP OAuth access token (`POST /mcp` only) and a SCIM token (`/scim/v2/*` only)."},"mcpOAuth":{"type":"oauth2","description":"MCP OAuth access token, used only by `POST /mcp` — not interchangeable with `bearerAuth`. Discovered per RFC 9728/8414 at `/.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server`.","flows":{"authorizationCode":{"authorizationUrl":"https://api.elaichi.ai/oauth/authorize","tokenUrl":"https://api.elaichi.ai/oauth/token","scopes":{"mcp:read":"Anything you can already see in Elaichi. Never secret values.","mcp:write":"Create and change teams, roles, toolboxes, templates and connections, invite people, and share access.","mcp:destructive":"Permanently delete data and remove people, inside your connected apps too. This cannot be undone.","mcp:tools":"Run tools from the toolboxes you choose next. Deleting needs the delete permission too.","openid":"That you are signed in, and the user ID we use for you. Nothing else.","email":"Your email address, and whether it is verified. Apps use this to check your domain."}}}}},"parameters":{"OrganizationIdHeader":{"name":"X-Organization-Id","in":"header","required":false,"description":"Target organization (`org_…`). Required for cookie/session auth on org-scoped routes; optional for API tokens, which carry their organization, and rejected when it names a different one.","schema":{"type":"string"}},"LimitQuery":{"name":"limit","in":"query","required":false,"description":"Page size. Defaults to 50. Values above 200 are silently clamped to 200 rather than rejected; a value below 1, or a non-integer, is a 400. Ignored by the endpoints documented as unpaginated.","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}},"CursorQuery":{"name":"cursor","in":"query","required":false,"description":"Opaque cursor — pass the `next_cursor` from a previous response. Ignored by unpaginated endpoints.","schema":{"type":"string"}}},"schemas":{"Error":{"type":"object","required":["error"],"description":"The Elaichi error envelope. The MCP OAuth endpoints under `/oauth` are the exception — they return RFC 6749 errors instead (see `OAuthError`).","properties":{"error":{"type":"object","required":["message","code"],"properties":{"message":{"type":"string","description":"Human-readable. 5xx messages are deliberately generic."},"code":{"type":"string","description":"Stable machine-readable code, e.g. `not_found`, `subscription_required`."},"details":{"description":"Structured detail for codes that carry one, e.g. `connections_need_resolution`."},"feature":{"type":"string","description":"The gated feature, on entitlement failures."},"plan":{"type":"string","enum":["gold","black","none"]}},"additionalProperties":true}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"RFC 6749 / 7591 / 8707 error code, e.g. `invalid_grant`, `invalid_client`, `invalid_redirect_uri`."},"error_description":{"type":"string"}},"additionalProperties":true},"ListEnvelope":{"type":"object","required":["result","next_cursor","prev_cursor"],"description":"Standard list envelope. Cursors are snake_case and opaque: pass `next_cursor` back as `cursor` to page forward. Both are null on unpaginated endpoints, which return the whole set in one response.","properties":{"result":{"type":"array","items":{}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]}}},"User":{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`)."},"email":{"type":"string","format":"email"},"name":{"type":["string","null"]},"avatar_url":{"type":["string","null"]},"identities":{"type":"array","description":"Login identities linked to this account.","items":{"type":"object","properties":{"provider":{"type":"string","description":"e.g. `google`, `email_code`, `invite`, or an SSO connection."},"connected_at":{"type":"string","format":"date-time"}}}},"is_active":{"type":"boolean","description":"Inactive accounts cannot authenticate at all."},"is_staff":{"type":"boolean","description":"Platform staff flag. Unrelated to organization roles."},"last_login_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Organization":{"type":"object","properties":{"id":{"type":"string","description":"Organization id (`org_…`) — the value for `X-Organization-Id`."},"name":{"type":"string"},"slug":{"type":"string","description":"Immutable after creation."},"plan":{"type":"string","description":"The stored plan: `gold`, `black`, or `none` when locked. This is not the same as the effective entitlement — read `GET /organization/{id}/entitlements` before gating a feature on it."},"region":{"type":["string","null"],"description":"Data location fixed at creation: `us`/`eu` pin a hard jurisdiction, `apac` is a placement hint."},"trial_ends_at":{"type":["string","null"],"format":"date-time"},"trial_indefinite_at":{"type":["string","null"],"format":"date-time","description":"When staff granted an indefinite trial, or null. Unlocked with no end date: no countdown, no expiry email, billing still reachable."},"settings":{"type":"object","additionalProperties":true,"description":"Free-form org settings. `PATCH /organization/{id}` REPLACES this object wholesale."},"logo":{"type":["string","null"],"format":"uri","description":"Public URL of the logo image, or null."},"can_delete":{"type":"boolean","description":"Whether the CALLER may delete this organization — the `org:delete` permission, which only the Org Owner role holds, and never for the platform root organization. Server-computed: branch on this rather than inspecting roles. Always `false` where the response has no member context to compute it from — the org switcher list `GET /organization` (one member-context lookup per row would be a per-row round trip), the staff console, and the invite-accept response. It never overstates: trust it when true, and read `GET /user/me` or `GET /organization/{id}` (both of which compute it) when you need it for a list row."},"deletion_scheduled_at":{"type":["string","null"],"format":"date-time","description":"Set when a deletion has been scheduled (`DELETE /organization/{id}`). The organization is read-only until `purge_after`. Null otherwise."},"purge_after":{"type":["string","null"],"format":"date-time","description":"When a scheduled deletion becomes permanent — 30 days after `deletion_scheduled_at`. Null unless scheduled."},"can_restore":{"type":"boolean","description":"Whether the CALLER may cancel a scheduled deletion: the `org:manage` permission, and only while `purge_after` is still ahead. Server-computed, like `can_delete`, and `false` for the same reasons — no member context (the org switcher list, the staff console) — plus once the window has closed. Read `purge_after` to tell \"too late\" from \"not yours to undo\"."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Role":{"type":"object","properties":{"id":{"type":"string","description":"Role id (`role_…`)."},"name":{"type":"string"},"description":{"type":["string","null"]},"is_system":{"type":"boolean","description":"Predefined roles ship with every organization and cannot be edited away."},"seat_class":{"type":"string","enum":["billable","free_admin"],"description":"Whether members holding only this role count toward billable seats."},"permissions":{"type":"array","items":{"type":"string"},"description":"Permission names from `GET /permission`. `PATCH /role/{id}` replaces this list wholesale."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"Member":{"type":"object","description":"One organization membership, enriched with the global user profile.","properties":{"user_id":{"type":"string","description":"User id (`usr_…`). A membership has no id of its own."},"status":{"type":"string","description":"Only `active` members can act in the organization."},"via":{"type":["string","null"],"description":"How they joined — invite, verified domain, SCIM, and so on."},"joined_at":{"type":"string","format":"date-time"},"roles":{"type":"array","description":"Roles held. Replace the whole set with `PATCH /member/{userId}`.","items":{"type":"object","properties":{"id":{"type":"string","description":"Role id (`role_…`)."},"name":{"type":"string"},"is_system":{"type":"boolean"},"seat_class":{"type":"string","enum":["billable","free_admin"]}}}},"teams":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":"string"}}}},"user":{"type":["object","null"],"description":"Global profile. Null when the profile row could not be resolved.","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":["string","null"]},"avatar_url":{"type":["string","null"]}}}}},"Team":{"type":"object","properties":{"id":{"type":"string","description":"Team id (`team_…`)."},"name":{"type":"string","description":"Unique within the organization."},"description":{"type":["string","null"]},"member_count":{"type":"integer","description":"How many people are in the team, counted server-side with one aggregate. This row carries no roster array to measure — `GET /team/{id}/member` is the roster."},"admin_count":{"type":"integer","description":"How many of them administer the team, counted the same way."},"admin_preview":{"type":"array","description":"The first few administrators, with display names already resolved, so a client can name them without a lookup per row. At most 5 — a preview for a hover, never the set; `admin_count` is the size. `name` and `email` are both null for someone who has left the organization.","items":{"type":"object","properties":{"user_id":{"type":"string"},"name":{"type":["string","null"]},"email":{"type":["string","null"]},"is_admin":{"type":"boolean","description":"Always true here."}}}},"member_preview":{"type":"array","description":"The same, for the first few members (administrators included, flagged by `is_admin`). `member_count` is the size; `GET /team/{id}/member` is the full, paginated roster.","items":{"type":"object","properties":{"user_id":{"type":"string"},"name":{"type":["string","null"]},"email":{"type":["string","null"]},"is_admin":{"type":"boolean"}}}},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"TeamMember":{"type":"object","properties":{"user_id":{"type":"string","description":"Elaichi user id (`usr_…`)."},"name":{"type":["string","null"],"description":"Null (together with `email`) for someone who has left the organization."},"email":{"type":["string","null"]},"is_admin":{"type":"boolean","description":"Team administrator: may rename the team and manage its ordinary members."},"joined_at":{"type":"string","format":"date-time","description":"When they joined THIS team."}}},"Success":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean"}}},"AccessLevel":{"type":"string","enum":["view","use","edit"],"description":"Same ladder for every shareable resource, low to high. `view` — see it exists, read its metadata/config; cannot exercise it. `use` — `view` + exercise it, resource-specific: run tools through a connection; execute a toolbox's tools through its bound connections (this DELEGATES — the caller runs through each entry's pinning editor's own authority, not necessarily their own); stamp a new toolbox by copying a template's entries; create connections from a connector. `edit` — `use` + change its settings, entries and its own grants."},"GranteeType":{"type":"string","enum":["user","team","org"],"description":"`user` and `team` grants target one `grantee_id`; `org` applies to every member of the organization and takes no id."},"AclEntry":{"type":"object","description":"One `resource_acl` row — a grant of access to a connection, toolbox, template or connector.","properties":{"id":{"type":"string","description":"ACL entry id — the `:aclId` a `DELETE .../share/{aclId}` call takes."},"resource_type":{"type":"string","enum":["connection","toolbox","template","connector"]},"resource_id":{"type":"string","description":"The shared resource's id (a connector's slug, for that type)."},"grantee_type":{"$ref":"#/components/schemas/GranteeType"},"grantee_id":{"type":["string","null"],"description":"User id (`usr_…`) or team id (`team_…`). Null for a `grantee_type: \"org\"` grant."},"level":{"$ref":"#/components/schemas/AccessLevel"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"ShareRequest":{"type":"object","required":["grantee_type","level"],"description":"Grants a user, a team, or the whole organization access to the resource at a level. `UNIQUE (resource, grantee_type, grantee_id)` — re-granting the same grantee replaces the level, it never stacks.","properties":{"grantee_type":{"$ref":"#/components/schemas/GranteeType"},"grantee_id":{"type":["string","null"],"description":"User id (`usr_…`) or team id (`team_…`). Omit or send null for `grantee_type: \"org\"`."},"level":{"$ref":"#/components/schemas/AccessLevel"}}}}}}