{"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. Authenticate with an organization API token (create one in the Elaichi app under Settings → API tokens) sent as a bearer token, plus the `X-Organization-Id` header on org-scoped routes; the MCP and OAuth endpoints use their own protocols. A few actions are deliberately not available to an API token and must be done by a person in the Elaichi app: managing API and SCIM tokens, creating an organization, approving an access request, deleting an organization, and changing the two-factor requirement. The operations concerned say so. Endpoints used only by the Elaichi app itself (sign-in, consent screens, browser redirects, inbound webhooks) are 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":"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":"Files"},{"name":"Logging destinations"},{"name":"Notification destinations"},{"name":"SSO connections"},{"name":"SCIM groups"},{"name":"SCIM v2"},{"name":"Group mapping"}],"paths":{"/health":{"get":{"operationId":"getHealth","summary":"Check API health","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. It does not check the service’s data stores, so a 200 means the API is up, not that every dependency is.","security":[]}},"/schema/openapi.json":{"get":{"operationId":"getOpenApiJson","summary":"Download 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":"Returns this OpenAPI document as JSON. 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":"Download 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":"Returns this OpenAPI document as YAML. The same document as `/schema/openapi.json`, served as YAML.","security":[]}},"/openapi.json":{"get":{"operationId":"getRootOpenApiJson","summary":"Download 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":"Download 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":"Get SAML service provider 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":"Get the current user and their organizations","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."},"mfa_setup_required":{"type":"boolean","description":"True when THIS session would be refused (`403 mfa_setup_required`) in at least one organization the user belongs to, because the organization requires a second factor and the session has not shown one. Always `false` for an API token. A client routes the person to second-factor setup on this rather than working out which organizations require one."},"mfa_setup_required_org_ids":{"type":"array","items":{"type":"string"},"description":"The organizations behind `mfa_setup_required`: a bounded page (at most 50), with the organization named by the `current_org_id` query parameter first when it is one of them. Not the whole set — `mfa_setup_required_org_count` is the true number; an organization not listed still refuses with `403 mfa_setup_required` when entered."},"mfa_setup_required_org_count":{"type":"integer","minimum":0,"description":"How many organizations refuse this session for lack of a second factor."},"can_disable_mfa":{"type":"boolean","description":"`false` while an organization the user belongs to requires a second factor and they hold no passkey — turning the authenticator app off (`DELETE /user/me/mfa/totp`) is refused with `409 mfa_required_by_organization`. The reason is in `mfa_disable_blocked_reason`."},"mfa_disable_blocked_reason":{"type":["string","null"],"description":"A sentence saying why `can_disable_mfa` is `false`, or `null`."},"is_root_org_staff":{"type":"boolean","description":"True only for Elaichi support accounts. Always false for customer members."},"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":"Returns the current user and their organizations. The result is 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. Needs an interactive browser session: an org API token is refused with 403, because a token scoped to one organization must not create another. Also needs public signup to be enabled for the service; 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` and `us` pin the organization's data to that jurisdiction, `apac` is a placement hint. It cannot be changed afterwards."},"referral_code":{"type":"string","pattern":"^[A-Za-z0-9_-]{1,64}$","description":"Optional referral code, for tracking only. Whitespace is trimmed, a blank value is treated as absent, and it is stored upper-cased. Recorded once at creation; not returned by this API and not editable."},"signup_source":{"type":"object","description":"Optional attribution for where the signup came from, for tracking only. Every field is optional. Malformed input is dropped, never a 400: unknown keys, non-string or blank values, and a `referrer_host` that is not a hostname are ignored. Values are cut to 100 characters; `referrer_host` is reduced to a lowercase hostname (a full URL is accepted and stripped to its host). Recorded once at creation; not returned by this API and not editable.","properties":{"utm_source":{"type":"string","maxLength":100},"utm_medium":{"type":"string","maxLength":100},"utm_campaign":{"type":"string","maxLength":100},"utm_term":{"type":"string","maxLength":100},"utm_content":{"type":"string","maxLength":100},"ref":{"type":"string","maxLength":100},"referrer_host":{"type":"string","maxLength":100}}}}}}}}},"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":"Lists 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, which is 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":"Updates an organization's name, settings or security options. Requires `org:manage` and an active Gold or Black subscription, except a body that sets only `allow_staff_impersonation` and/or `mfa_required`, which is accepted whatever the plan. **`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` and `allow_staff_impersonation` are normal field updates, and omitting a field leaves it untouched. `mfa_required` is a sign-in policy with its own rules: changing it needs an interactive browser session (an org API token answers 403, and the AI surfaces do not accept the field at all), it is refused to an Elaichi support session, turning it off needs step-up reauthentication in the app (`428 step_up_required`), and turning it on answers `409 mfa_setup_required` when the caller's own session would fail the check; see `can_require_mfa` on the response. A change is recorded in the `organization.updated` audit entry, with the old and new value in its `mfa_required` metadata. `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."},"allow_staff_impersonation":{"type":"boolean","description":"`false` refuses Elaichi support access to this organization: every request made through a support session is refused, reads included. Recorded in the `organization.updated` audit entry: the field is named in `fields` and the value it was set to is in the entry's `allow_staff_impersonation` metadata."},"mfa_required":{"type":"boolean","description":"`true` requires every member to hold a second factor to act in this organization; `false` lifts it. See the operation description for who may change it and what a change needs."}}}}}},"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 full teardown will run, 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.","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. There are only a handful of possible steps, so the list stays short however large the organization is; a step that walks many items reports a count and a few sample ids in its single entry.","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 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.** Immediately and irreversibly, every connection is severed and its stored 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`. MCP, tool execution and new connections are refused outright. On day 30 the full teardown runs. 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`), an interactive browser session (an org API token is refused with 403), and step-up reauthentication in the app: without a valid `X-Step-Up-Token` the call answers `428 step_up_required`. `confirm_slug` must equal the organization's own `slug`; the API re-checks it, so a client that skips its confirmation prompt does not skip the check. Unlike every other mutation here this does not require an active subscription, so a locked or expired organization can still 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, so tell the user 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`. The API re-checks it."},"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, because their stored credentials were deleted at the original delete; 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. 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 suspended, and with `subscription_required` when the plan lapsed (the delete canceled the Stripe subscription, so a restore needs a new one).","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":"Uploads the organization's logo. The body is the raw image (PNG, JPEG or WebP, at most 2 MiB), not multipart. Returns the organization with `logo` set to the public URL the image is served from. The previous logo 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":"The Gold trial before a subscription starts."},"trial_indefinite":{"type":"boolean","description":"Elaichi-granted indefinite trial: unlocked with no end date. `trial_active` stays false because there is no clock to count down."},"features":{"type":"object","description":"Feature key → allowed. A key that is absent is not offered to this organization — not an error. The response can carry further informational fields beyond those listed here.","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":"Returns 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. 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":"Get 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 Gold trial before a subscription starts. 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 billing is not enabled for the service."},"seat_count":{"type":"integer","description":"Billable memberships, with the paid-subscription minimum of one. This is the quantity billed."},"available_plans":{"type":"object","description":"Which plans and billing intervals can be purchased.","properties":{"gold":{"type":"boolean"},"gold_monthly":{"type":"boolean"},"gold_yearly":{"type":"boolean"},"black":{"type":"boolean"}}},"pricing":{"type":"object","required":["country","tier","discount_pct","regional","available","currency","gold","list"],"properties":{"country":{"type":["string","null"],"description":"ISO 3166-1 alpha-2 the price is based on, or null when unknown (which prices as list)."},"tier":{"type":"string","description":"Tier id: `t0` (list) to `t4` by GDP band, or a country tier such as `in` (India). Compare by `discount_pct`."},"discount_pct":{"type":"integer","description":"Discount off list, 0–60."},"regional":{"type":"boolean","description":"`tier !== \"t0\"`."},"available":{"type":"boolean","description":"False only for a country Elaichi cannot sell to; a purchase from there is refused with `region_unavailable`."},"currency":{"type":"string","description":"Lowercase ISO 4217 the `gold` amounts are in."},"gold":{"type":"object","properties":{"month":{"type":"object","properties":{"unit_amount":{"type":"integer"}}},"year":{"type":"object","properties":{"unit_amount":{"type":"integer"}}}},"description":"Per-seat price for one cycle in `currency` minor units, exactly what is charged. `year` is the year total."},"list":{"type":"object","description":"List (t0) price to show the discount against: in `currency` when list price is set in it, else USD. Display only.","properties":{"currency":{"type":"string"},"gold":{"type":"object","properties":{"month":{"type":"object","properties":{"unit_amount":{"type":"integer"}}},"year":{"type":"object","properties":{"unit_amount":{"type":"integer"}}}}}}}},"description":"What this org's next Gold purchase charges: the tier from its first purchase if it has one, otherwise the tier for the country the request comes from."}}}}}},"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 organization's subscription and seat state. 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_…`)."}]}},"/permission":{"get":{"operationId":"listPermissions","summary":"List permissions","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 that is only `connector:create`: a connector is governed by who may use it, not by the destination it calls. Always present; show 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":"Lists the permission names a role may hold, with display labels. This is the source of truth for validating `permissions` on `POST /role` and `PATCH /role/{id}`. The catalog is per organization: it lists the permissions this organization may grant. Paged with `limit` and `cursor`; read every page for the whole catalog.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"}]}},"/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"}}}}},"description":"Returns one role with its permissions and member counts. Any member may read it.","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":"Updates a custom role. **`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 immediately: effective permissions are recomputed on every request. 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":"OK","content":{"application/json":{"schema":{"type":"object","required":["success","reassigned_member_count","reassigned_invite_count"],"properties":{"success":{"type":"boolean"},"reassigned_member_count":{"type":"integer","description":"How many members were moved onto `reassign_to`. 0 when none held the role."},"reassigned_invite_count":{"type":"integer","description":"How many pending invites were re-pointed at `reassign_to`."},"reassigned_by_role":{"type":"array","items":{"type":"object","properties":{"role_id":{"type":"string"},"role_name":{"type":"string"},"count":{"type":"integer"}}},"description":"One row per distinct role members were moved onto (just `reassign_to` here). Empty when nobody held the 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":"Deletes a custom role. Irreversible. A member holds exactly one role, so a role anybody still holds cannot simply disappear: without `reassign_to` the call answers `400 role_in_use` while `member_count` or `pending_invite_count` is above zero (`GET /member?role_id={id}` lists the holders). With `reassign_to`, every holder and every pending invite for the role is moved onto that role and the role is deleted in one atomic operation — everything moves and the role is gone, or nothing changes. The move takes effect on each member's next request. A role that a SCIM group mapping assigns (`group_mapping_count` above zero) is refused with `409 role_mapped`, `reassign_to` or not: change the mapping first. Requires `role:manage` and the `custom_roles` feature. Sending `reassign_to` at all additionally requires `member:manage` and that you hold every permission the replacement role carries (the `PATCH /member/{userId}` rules), and the target must exist and differ from `id` — checked whether or not anything needs moving. It is further refused if you hold the deleted role yourself, if the replacement is Org Owner and you are not one, or if it would leave no billable member. System roles cannot be deleted. This is the query-parameter-only form; `POST /role/{id}/delete` is the same operation with a JSON body, for a client that also wants to move specific holders onto a role of their own choosing. A browser session must also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Role id (`role_…`)."},{"name":"reassign_to","in":"query","required":false,"description":"Role id (`role_…`) to move every current holder and pending invite onto before deleting. Must exist and differ from `id`; its authorization (`member:manage` and the grant ceiling) applies even when nothing holds the role, in which case nothing is moved.","schema":{"type":"string"}}]}},"/role/{id}/delete":{"post":{"operationId":"deleteRoleStructured","summary":"Delete a role, with a JSON body","tags":["Roles"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["success","reassigned_member_count","reassigned_invite_count","reassigned_by_role"],"properties":{"success":{"type":"boolean"},"reassigned_member_count":{"type":"integer"},"reassigned_invite_count":{"type":"integer"},"reassigned_by_role":{"type":"array","items":{"type":"object","properties":{"role_id":{"type":"string"},"role_name":{"type":"string"},"count":{"type":"integer"}}},"description":"One row per distinct target role that received at least one 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":"Deletes a role, taking a JSON body. The same operation as `DELETE /role/{id}` — same authorization, same guards, same atomic transaction — with a JSON body instead of a query parameter, so a caller can also move SPECIFIC holders onto a role of their own choosing before the rest fall back to `reassign_to`. `reassignments` is capped at 200 entries; past it the call answers `400` naming the ceiling. Each entry's `role_id` is checked against the same grant ceiling as `reassign_to` (never the role being deleted), and each `user_id` must currently hold the role — one that no longer does answers `409` (`role_holder_changed`), so the caller can refresh and retry. `reassign_to` becomes optional once every holder is covered by `reassignments` AND no pending invite is for the role (invites are never individually overridden, and always fall back to `reassign_to`); short of that it is still required, exactly as on the plain DELETE form. The response adds `reassigned_by_role`: one row per DISTINCT target role actually used, bounded by how many roles the caller named.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reassign_to":{"type":"string","description":"Same as `DELETE /role/{id}`'s `reassign_to` query parameter — the default target."},"reassignments":{"type":"array","maxItems":200,"items":{"type":"object","required":["user_id","role_id"],"properties":{"user_id":{"type":"string","description":"User id (`usr_…`) of a current holder of `id`."},"role_id":{"type":"string","description":"Role id (`role_…`) to move that one member onto."}}},"description":"Per-member overrides. Bounded, and never a substitute for `reassign_to` on invites."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Role id (`role_…`) being deleted."}]}},"/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"}}}}},"description":"Lists the organization's teams, each with a member count and a few administrator names. Any member may read them. Cursor-paginated; `q` matches the team name.","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":"Creates a team. Requires `team:create` (every Member holds it) or `team:manage`. Team names are unique within the organization. The caller becomes this team's administrator immediately — the same auto-ownership every other created resource gets — whether or not their own id also appears in `member_ids`. `member_ids` optionally adds other existing organization members (`GET /member`) as ordinary members in the same call; an id that is not already an org member fails the whole call before anything is created. To promote one of them to administrator too, follow up with `PATCH /team/{id}/members/{userId}`. There is no way to create a team you do not administer: to stand one up for somebody else, create it and appoint them; a team administrator cannot remove another administrator, themselves included, so taking yourself off afterwards needs a `team:manage` holder.","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},"member_ids":{"type":"array","maxItems":50,"items":{"type":"string","minLength":1},"description":"User ids (`usr_…`) of existing organization members to add alongside the caller. Optional."}}}}}},"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"}}}}},"description":"Returns one team with its member and administrator counts. Any member may read it; `404` when the team does not exist. The roster itself is `GET /team/{id}/member`.","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":"Renames a team or changes its 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":"Deletes a team. 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. Allowed for `team:manage` or an administrator of this team. A browser session must also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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":"Lists the members of a team. 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, manage its ordinary members, appoint further administrators and delete it, and may manage team-scoped connections. Appointing (`is_admin: true`) is allowed for `team:manage` or an administrator of this team; standing an administrator down (`is_admin: false`) requires `team:manage`. Nobody may appoint themselves.","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}},{"name":"role_id","in":"query","required":false,"description":"Only members holding this role (`role_…`) — where a role's `member_count` leads. Combines with `q`; a `q` matching more members than the search ceiling answers 400 rather than a partial list.","schema":{"type":"string"}},{"name":"second_factor","in":"query","required":false,"description":"Only `none`: members who hold neither an authenticator app nor a passkey, the list to follow up when turning on the two-factor requirement. Paginated like any other filter; combines with `q`, not with `role_id` (400). Requires `org:manage`.","schema":{"type":"string","enum":["none"]}}]}},"/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":"Sets a member's role. **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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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":"`<kind>_need_resolution` (toolboxes, templates, collections, dashboards, knowledge_bases, bundles) — a shared owned resource has no decision; `error.details.<kind>` lists each. `synthetic_tools_need_resolution` — another member's toolbox pins a synthetic tool of theirs; acknowledge it. Also `connectors_need_resolution` — the member owns custom connectors with no transfer; `error.details.connectors` lists each, and the retry carries a `connector_actions` transfer per connector, or `defaults.transfer_to_user_id` for all of them. `member_owns_connectors` is the same refusal from the org itself, for a connector that arrived after the summary was read. `connectors_need_resolution` carries `error.details.no_eligible_successor`: true when nobody can take a connector — no active member besides the leaver and the acting admin, and the admin lacks `connector:manage` — so no `connector_actions` body can satisfy it: invite another member first, or have the leaver transfer their connectors before they leave. `error.details.sole_successor` is true when the acting admin is the only other member and holds `connector:manage`: they may then name themselves as the recipient of connectors (only connectors; every other resource type still refuses a self-transfer).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Removes a member from the organization. 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 connections are always deleted (with their stored credentials); a `connection_actions` entry saying `transfer` is refused with 400 `connection_transfer_removed`. To keep what used a connection working, name a replacement in `connection_replacements`: before the old connection is deleted, every toolbox entry and other member's synthetic tool step that names it is rewritten onto the replacement (toolbox entries are re-stamped with you as delegator). A replacement must be the same connector, active, not the leaver's, and usable by you (400 `replacement_*` otherwise) — the preflight's `replacement_candidates` are exactly those. Everything else they own goes to a person (`target_user_id`) or a team (`target_team_id`: the team's first active admin owns it and the team gets an `edit` share; 400 `team_has_no_admin` when nobody can) per row, or to one bulk `defaults.transfer_to_user_id` / `transfer_to_team_id`. A shared owned resource with no decision refuses with 409 `<kind>_need_resolution`. Requires `member:manage`. A browser session must also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged. The step-up check runs last, after every resolution refusal above, so an admin never re-authenticates only to be told the removal needs decisions first. Custom connectors the member owns are resolved by `connector_actions` (`transfer` only — never delete, and never released: an ownerless connector is a legacy state). EVERY owned connector, private or shared, needs a new owner: one with no entry is refused with 409 `connectors_need_resolution` unless `defaults.transfer_to_user_id` names a bulk recipient. A body that still sends `action: \"release\"` or `defaults.release_connectors` is refused with 400 `connector_release_removed` (the message says what to send instead). Should the member still own a connector when the removal reaches the org itself, that is 409 `member_owns_connectors`. The response `applied` counts include `connectors`.","security":[{"bearerAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"connection_actions":{"type":"array","maxItems":200,"description":"Optional: every connection the member owns is deleted whether named or not.","items":{"type":"object","required":["connection_id","action"],"properties":{"connection_id":{"type":"string","description":"Connection id (`conn_…`) from the offboarding preflight."},"action":{"type":"string","enum":["delete"],"description":"The only action. `transfer` answers 400 `connection_transfer_removed`."}}}},"connection_replacements":{"type":"array","maxItems":200,"description":"Before each old connection is deleted, rewrite every toolbox entry and other member's synthetic tool step naming it onto the replacement. One entry per old connection.","items":{"type":"object","required":["connection_id","replacement_connection_id"],"properties":{"connection_id":{"type":"string","description":"The leaver's connection (`conn_…`)."},"replacement_connection_id":{"type":"string","nullable":true,"description":"Same connector, active, not the leaver's, usable by the caller. `null` keeps this one connection out of its app's `replacement_defaults` entry: the tools that used it stop."}}}},"toolbox_actions":{"type":"array","maxItems":200,"description":"Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.","items":{"type":"object","required":["resource_id","action"],"properties":{"resource_id":{"type":"string","description":"Id from the offboarding preflight."},"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"A person (`usr_…`): an active member other than the leaver and the caller."},"target_team_id":{"type":"string","description":"A team (`team_…`): its first active admin owns it, the team gets an `edit` share. Never with `target_user_id`."}}}},"template_actions":{"type":"array","maxItems":200,"description":"Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.","items":{"type":"object","required":["resource_id","action"],"properties":{"resource_id":{"type":"string","description":"Id from the offboarding preflight."},"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"A person (`usr_…`): an active member other than the leaver and the caller."},"target_team_id":{"type":"string","description":"A team (`team_…`): its first active admin owns it, the team gets an `edit` share. Never with `target_user_id`."}}}},"collection_actions":{"type":"array","maxItems":200,"description":"Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.","items":{"type":"object","required":["resource_id","action"],"properties":{"resource_id":{"type":"string","description":"Id from the offboarding preflight."},"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"A person (`usr_…`): an active member other than the leaver and the caller."},"target_team_id":{"type":"string","description":"A team (`team_…`): its first active admin owns it, the team gets an `edit` share. Never with `target_user_id`."}}}},"dashboard_actions":{"type":"array","maxItems":200,"description":"Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.","items":{"type":"object","required":["resource_id","action"],"properties":{"resource_id":{"type":"string","description":"Id from the offboarding preflight."},"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"A person (`usr_…`): an active member other than the leaver and the caller."},"target_team_id":{"type":"string","description":"A team (`team_…`): its first active admin owns it, the team gets an `edit` share. Never with `target_user_id`."}}}},"knowledge_base_actions":{"type":"array","maxItems":200,"description":"Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.","items":{"type":"object","required":["resource_id","action"],"properties":{"resource_id":{"type":"string","description":"Id from the offboarding preflight."},"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"A person (`usr_…`): an active member other than the leaver and the caller."},"target_team_id":{"type":"string","description":"A team (`team_…`): its first active admin owns it, the team gets an `edit` share. Never with `target_user_id`."}}}},"bundle_actions":{"type":"array","maxItems":200,"description":"Per-resource decisions; a shared one with none (and no bulk target) refuses the removal.","items":{"type":"object","required":["resource_id","action"],"properties":{"resource_id":{"type":"string","description":"Id from the offboarding preflight."},"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"A person (`usr_…`): an active member other than the leaver and the caller."},"target_team_id":{"type":"string","description":"A team (`team_…`): its first active admin owns it, the team gets an `edit` share. Never with `target_user_id`."}}}},"connector_actions":{"type":"array","maxItems":200,"description":"One decision per custom connector the member owns (see `connectors` on the offboarding preflight). Required for every connector unless `defaults.transfer_to_user_id` covers it.","items":{"type":"object","required":["connector_slug","action"],"properties":{"connector_slug":{"type":"string","description":"Connector slug from the offboarding preflight."},"action":{"type":"string","enum":["transfer"],"description":"`transfer` hands ownership to `target_user_id` (grants untouched). There is no delete, and no release: `release` was removed and answers 400 `connector_release_removed`."},"target_user_id":{"type":"string","description":"For `transfer` (`usr_…`) — an active member other than the one being removed and other than the caller."},"target_team_id":{"type":"string","description":"For `transfer`: a team (`team_…`) instead of a person."}}}},"replacement_defaults":{"type":"array","maxItems":200,"description":"One replacement per app instead of one `connection_replacements` entry per connection: every connection of `connector_slug` with no `connection_replacements` entry of its own is replaced by `replacement_connection_id`, validated per connection exactly like an explicit entry. A replacement the rules refuse is a 400 for the whole request (`replacement_*`); an app named twice is 400 `duplicate_replacement_default`; an app the member has no connection of is 400 `replacement_default_unknown_connector`.","items":{"type":"object","required":["connector_slug","replacement_connection_id"],"properties":{"connector_slug":{"type":"string","description":"The app, from the preflight's connections."},"replacement_connection_id":{"type":"string","description":"One of that app's `replacement_candidates`."}}}},"kind_defaults":{"type":"object","description":"One decision per KIND for every item of it with no per-resource entry. Per item: its own entry wins, then its kind's default, then `defaults`. A transfer names exactly one of `target_user_id` / `target_team_id`; a delete names neither (400 otherwise). Unlike `defaults`, a kind default may delete — it names one kind deliberately — and a delete reaches only the items in the summary the caller read: one created since is left for the next round (202).","properties":{"toolbox":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"For `transfer`: a person (`usr_…`)."},"target_team_id":{"type":"string","description":"For `transfer`: a team (`team_…`) instead of a person."}}},"template":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"For `transfer`: a person (`usr_…`)."},"target_team_id":{"type":"string","description":"For `transfer`: a team (`team_…`) instead of a person."}}},"collection":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"For `transfer`: a person (`usr_…`)."},"target_team_id":{"type":"string","description":"For `transfer`: a team (`team_…`) instead of a person."}}},"dashboard":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"For `transfer`: a person (`usr_…`)."},"target_team_id":{"type":"string","description":"For `transfer`: a team (`team_…`) instead of a person."}}},"knowledge_base":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["transfer","delete"]},"target_user_id":{"type":"string","description":"For `transfer`: a person (`usr_…`)."},"target_team_id":{"type":"string","description":"For `transfer`: a team (`team_…`) instead of a person."}}},"connector":{"type":"object","required":["action"],"properties":{"action":{"type":"string","enum":["transfer"],"description":"Custom connectors are never deleted."},"target_user_id":{"type":"string","description":"A person (`usr_…`)."},"target_team_id":{"type":"string","description":"A team (`team_…`) instead of a person."}}}}},"defaults":{"type":"object","description":"Bulk decisions for every resource that carries no explicit per-resource action.","properties":{"transfer_to_user_id":{"type":"string","description":"One active member (`usr_…`) who inherits everything owned that has no explicit action. Connections never follow it."},"transfer_to_team_id":{"type":"string","description":"Or one team (`team_…`): its first active admin owns everything, the team gets an `edit` share. Never with `transfer_to_user_id`."},"delete_synthetic_tools":{"type":"boolean","description":"Acknowledge, once, that every synthetic tool is deleted with its author."}}}}}}}},"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":"Preview what removing a member would break","tags":["Members"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"user_id":{"type":"string"},"connections":{"type":"array","description":"Every connection the member owns: `connection`, `referenced_by`, `private`, `needs_resolution` (informational), `references`, `replacement_candidates`, `replacement_candidate_count`.","items":{"type":"object","additionalProperties":true}},"no_eligible_successor":{"type":"boolean","description":"True when the member owns connectors and nobody can receive them: no active member other than the leaver and the caller exists, and the caller lacks `connector:manage` so cannot take them either. `DELETE /member/{userId}` cannot succeed until another member is invited. Always false when `connectors` is empty. A bounded existence check, never a member count."},"sole_successor":{"type":"boolean","description":"True when the member owns connectors, no active member other than the leaver and the caller exists, and the caller holds `connector:manage`: the caller may then name themselves as the connector recipient (audited as `self_assigned_sole_successor`). Connectors only. Mutually exclusive with `no_eligible_successor`; always false when `connectors` is empty."},"connectors":{"type":"array","description":"Every custom connector the member owns: `slug`, `name`, `logo`, `owner_user_id`, `private`, the bounded `shared_with` rollup, `connection_count`, `needs_resolution` (always true: every connector needs a new owner) and timestamps. Resolved by `connector_actions` on `DELETE /member/{userId}`.","items":{"type":"object","additionalProperties":true}},"delegated_entries":{"type":"array","description":"Non-blocking: 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":"Previews what removing this member would break, before anything is changed. Lists every connection they own (all are deleted on removal), each with `references` (exact counts of the toolboxes and other members' synthetic tools using it, plus at most five names) and up to ten `replacement_candidates` (same connector, active, not the leaver's, usable by you; owner named) beside an exact `replacement_candidate_count`. `credentials` counts the API tokens and AI client logins that removal revokes. Separately, `delegated_entries` lists entries on other members' toolboxes whose pin currently rides on this member's own `use` grant, not their ownership. This is non-blocking, and 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; nothing does that automatically. `connectors` lists every custom connector the member owns, each with a short `shared_with` summary and a `connection_count`. Read-only: call it before `DELETE /member/{userId}` to collect the hand-over and replacement decisions that call takes. 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":"Lists pending (not yet accepted) invites, cursor-paginated. 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 an app 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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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 a 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`. A browser session must also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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 a 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":"Revokes a pending invite. Irreversible: the emailed link stops working immediately. Requires `member:manage`. A browser session must also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged. A factor proved in the last ten minutes satisfies it, so revoking several invites in a row prompts once.","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":"Lists the domains this organization has claimed, verified or not, cursor-paginated. Requires `org:manage` or `sso:view`. Claiming, verifying, editing and removing a domain all require `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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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":"Updates the defaults of a claimed org domain. 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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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":"Deletes a claimed org domain. 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 an org domain","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":"Verifies a claimed org domain. 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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Org domain id."}]}},"/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"],"description":"`staff` means Elaichi support did it: either directly (a `staff.*` action, where `actor_user_id` is the support user, not a member) or while signed in as a member (`actor` is the member and `metadata.impersonated_by_user_id` is the support user)."},"actor":{"type":"object","description":"The person behind this row. Read this rather than resolving `actor_user_id` against the member directory, which is paginated and would only name the actors on the page you 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"]},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."},"kind":{"type":"string","description":"Mirrors `actor_kind`, so a non-human actor stays distinguishable from an unnamed one."}}},"tool_call":{"type":["object","null"],"description":"A connected-tool call (`assistant.toolbox_tool`, `assistant.toolbox.execute`) or synthetic-tool run (`synthetic_tool.executed`) in words; null on every other row. Names are resolved when you read, so renaming or deleting a connection or client changes how old rows read.","properties":{"summary":{"type":"string","description":"Outcome-aware sentence, e.g. \"Updated a page in Notion\" or \"Failed to update a page in Notion\"."},"tool_name":{"type":"string","description":"The catalog tool name, unqualified, when recorded; else the advertised name."},"connector":{"type":["object","null"],"description":"Null when no single connector applies (a synthetic tool) or none was recorded or resolvable.","properties":{"slug":{"type":"string"},"name":{"type":"string"},"logo":{"type":["string","null"]},"can_view":{"type":"boolean","description":"Whether the caller may open this connector (`GET /connector/{slug}`): holds `connector:view` and the catalog resolved the slug."}}},"toolbox":{"type":["object","null"],"description":"The stored toolbox the call went through. Null for the built-in all-tools and per-connection sets, for a synthetic-tool row, and for a row that never recorded one.","properties":{"id":{"type":"string"},"name":{"type":["string","null"],"description":"Null when the toolbox is gone or the lookup failed."},"can_view":{"type":"boolean","description":"Whether the caller may open it (`GET /toolbox/{id}`): owner or a grant."}}},"synthetic_tool":{"type":["object","null"],"description":"The synthetic tool the call ran: a `synthetic_tool.executed` row, or a toolbox call whose tool was a synthetic one. Null for every ordinary connector-tool call.","properties":{"id":{"type":"string"},"name":{"type":"string","description":"The name it had when it ran."},"can_view":{"type":"boolean","description":"Whether the caller may open it (`GET /synthetic-tool/{id}`): its owner only."}}},"connection":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"state":{"type":"string","enum":["active","deleted","unknown"],"description":"`unknown` means the lookup failed — never read it as deleted."},"can_view":{"type":"boolean","description":"Whether the caller may open this connection (`GET /connection/{id}`)."}}},"via":{"type":"object","properties":{"surface":{"type":["string","null"],"enum":["app","mcp","api","console",null],"description":"`app` the Elaichi app's built-in agent, `console` a person using the Elaichi app itself (a synthetic tool's Run), `mcp` an MCP client, `api` an org API token. Null when not recorded, never guessed."},"client_id":{"type":["string","null"]},"client_name":{"type":["string","null"],"description":"The verified client name when its redirect URIs prove it, else the self-reported name."},"client_logo":{"type":["string","null"]},"client_mark":{"type":["string","null"],"enum":["claude","chatgpt","cursor","grok","perplexity","mistral","devin",null],"description":"The verified client's bundled-mark slug; set only when `client_verified` is true."},"client_verified":{"type":"boolean","description":"True only when the name and logo come from the verified client."}}},"outcome":{"type":"string","enum":["succeeded","failed"]},"error":{"type":["string","null"],"description":"Error code (never a message), e.g. `call_failed:429`."},"error_message":{"type":["string","null"],"description":"Fixed plain-English sentence for `error`; null on success."},"access":{"type":["string","null"],"enum":["read","write","destructive",null]},"approval":{"type":["string","null"],"enum":["not_required","approved_by_person","always_allowed","allowed_by_connected_app","direct_run",null],"description":"`direct_run`: a person ran a synthetic tool directly (in the Elaichi app or with `POST /synthetic-tool/{id}/execute`), so no approval gate asked; this is not the same as `not_required` (a read). On `assistant.toolbox.execute` the requirement follows the tool's own tier. Null when the row predates the recorded facts, or the source is not one of these."},"arguments_edited":{"type":["boolean","null"]},"target":{"type":["object","null"],"description":"What the call touched. A list, search or query has the type alone (id, name and url null). Null when nothing could be identified.","properties":{"type":{"type":["string","null"],"description":"The catalog resource, singular."},"id":{"type":["string","null"],"description":"The path id or the created record's id. Never an email address."},"name":{"type":["string","null"],"description":"The object's title. Always null for a person's record (by resource noun or by response shape), for a message-like record (email, thread, comment, event, post, ...), and when it contains an email address."},"url":{"type":["string","null"],"description":"http(s), no user:password, origin + path only (query and fragment dropped). Null for a create-like call and for any call whose resource or method names a session, signed/temporary/download link, export, portal, checkout, invite or recipient view."}}},"result_count":{"type":["integer","null"],"description":"Items a list, search or query returned."}}}}}},"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":"Lists what happened in this organization, newest first: an append-only trail. Open to every member: with `audit:view` it lists every event in the organization; without it, only the caller's own events plus events on connections they own or can edit. The response says which, as a top-level `scope` (`org` or `own`) beside the list envelope. An `own` response also carries `connection_scope`: `all`, `one` (a `connection_id` was given), or `too_many` (the caller owns or can edit more connections than one query lists, so the page holds only their own events). Anyone who can edit a connection (the owner, or an `edit` grant direct, through a team, or org-wide) sees every member's tool calls made through it. 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, so 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","file","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","read","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":"connection_id","in":"query","required":false,"description":"Only events on this connection (`conn_…`). Needs `audit:view`, or owning the connection, or `edit` on it; otherwise 404, the same as an id that does not exist. 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. Always this string — the owning MEMBER is `owner_member`, a separate field, so a client never has to tell a string from an object."},"owner_member":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":["string","null"]},"email":{"type":"string","format":"email"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},{"type":"null"}],"description":"The resolved owning member, the same shape as a toolbox's `owner`. Org-owned rows the caller reaches (ownership or a grant) only: an object when somebody OTHER than the caller owns the connector (just `{ id }` for an owner who has left the org); `null` when the connector has no owner yet (an ownerless connector — see `POST /connector/{slug}/owner`); absent when the caller owns it (`owner_user_id` already says so) or reaches it by nothing but `connector:view`."},"custom":{"type":"boolean","description":"True for a connector this organization authored or forked."},"kind":{"type":"string","enum":["catalog","remote_mcp"],"description":"`remote_mcp` for an organization's own bring-your-own remote MCP connector (its tools are discovered per connection, not read from the catalog), `catalog` for every other connector. Read it instead of guessing from the slug. On the detail, a remote MCP connector also carries `remote_mcp` (`url`, `auth_mode`, `oauth_registration`, `catalog_owner`, `listed_tool_count`, the number of tools `GET /connector/{slug}/tool` lists to this caller with no filter (turned-off tools included for a caller who can manage the connector), and `support_contact`, the email or https link Elaichi has on file for a connector it publishes, null otherwise) and `auth`. Its `tool_count` is the number of tools that are switched on."},"byoa":{"type":"boolean","description":"True when the connector needs the customer's own OAuth app (bring your own app): a placeholder client id, a real id with a placeholder secret, or a real id with no platform OAuth secret stored for it."},"single_redirect_auth":{"type":"boolean","description":"A hint, not a promise: true when connecting this connector is most likely a provider sign-in with nothing to fill in first — exactly one auth method, and that method can redirect (OAuth). A list row judges the auth methods only; `GET /connector/{slug}` also rules out a method with form fields or a `permissions_text` notice. A client may use it to get a popup window ready inside the click that starts a connect. `direct_connect_url` on the `POST /connection` / reconnect response stays authoritative."},"can_fork":{"type":"boolean","description":"Whether the caller may fork the connector. False for a remote MCP connector (`POST /connector/{slug}/fork` answers `409 connector_not_forkable`), for a connector the caller is restricted from, without `connector:create`, and when the plan does not include custom connectors. On `GET /connector` rows and `GET /connector/{slug}`."},"owner_user_id":{"type":"string","description":"Owning member (`usr_…`). Present only on an org-owned row the caller reaches (ownership or a grant) and that has an owner. An ownerless connector has none until `POST /connector/{slug}/owner` assigns one, and a member browsing with `connector:view` alone is not told whose it is. The owner has implicit `edit`, plus delete and transfer, which no share confers."},"can_use":{"type":"boolean","description":"Whether the caller may create (or re-authenticate) a connection from this connector. That needs the `use` level that `POST /connection` and `POST /connection/{id}/reconnect` enforce (403 `connector_not_shared` otherwise): ownership or a `use`-or-better grant (direct, team or org-wide) on an org-owned connector, and always true on a platform catalog row. This is about sharing and is independent of `restricted`: a connector shared for viewing only is listed but cannot be connected, so leave Connect off rather than show a button that refuses. A connector nobody shared with the caller is not listed."},"can_manage":{"type":"boolean","description":"Present only on org-owned/custom connector rows (omitted for platform catalog rows, which have no sharing). Whether the caller may edit this connector's config: ownership or an `edit` grant of their own. The `connector:manage` permission alone does not imply it, so org owners and admins do not manage a connector nobody shared with them."},"can_transfer":{"type":"boolean","description":"Org-owned rows only. Whether the caller may transfer this connector — ownership, full stop; no `connector:manage` fallback. Mirrors `POST /connector/{slug}/transfer`. False for everyone on an ownerless connector (transfer needs an owner). Not the delete gate — see `can_delete`."},"can_delete":{"type":"boolean","description":"Org-owned rows only. Whether the caller may delete this connector: the owner, and only the owner, so false for everyone on an ownerless connector. Mirrors `DELETE /connector/{slug}`."},"can_assign_owner":{"type":"boolean","description":"Org-owned rows only. Whether the caller may give this ownerless connector an owner: it has no owner row AND the caller holds `connector:manage`. Mirrors `POST /connector/{slug}/owner` naming another member. Independent of the caller's reach, so a manager browsing the catalog can see that a connector needs an owner; it says nothing about who has access."},"can_claim":{"type":"boolean","description":"Org-owned rows only. Whether the caller may make THEMSELVES the owner: `can_assign_owner` AND a real `edit` grant of their own. `connector:manage` alone never makes its holder the owner of a connector nobody shared with them. Mirrors `POST /connector/{slug}/owner` naming the caller."},"can_revoke_share":{"type":"boolean","description":"Org-owned rows only. Whether the caller may grant or revoke access to this connector: `edit` access or ownership, and `connector:share`. This is separate from `can_manage`: an `edit` grantee whose role omits `connector:share` can reconfigure the connector but cannot change who else has access. Mirrors `DELETE /connector/{slug}/share/{aclId}`."},"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."},"restricted_inherited_from":{"type":["object","null"],"properties":{"slug":{"type":["string","null"],"description":"The upstream connector the restriction is inherited from. For API consumers; never render it. Null — together with `label` — when the caller may not be told the upstream (an org-owned upstream they hold no grant on, or one that was deleted): a slug names the connector as surely as its label does."},"label":{"type":["string","null"],"description":"The upstream's display name — null, together with `slug`, when the caller may not be told it. The UI then says \"a connector this was forked from\"."}},"required":["slug","label"],"description":"Set when this connector is restricted for the caller only because the connector it was forked from is. A fork inherits its upstream’s restrictions on purpose (forking a blocked connector is not a bypass), but nothing was set on the fork itself, so this says where the block came from. Null when not restricted, when a rule names this connector directly, or when the upstream could not be traced. Asking for access still targets this connector: approving the fork’s own request opens that fork for that person only (whether the block comes through their role or a rule written about them personally) and the original stays restricted for them; the approved request reads `restriction_lifted: true`. A sibling of `restricted_by`, which stays the precedence layer. Do not derive it yourself from the fork’s upstream."},"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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the connector reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Lists the platform catalog plus this organization's own custom connectors. Every member sees the platform catalog. A custom connector is listed only for its owner and the members it is shared with, and is absent (and `404` by slug) for everyone else, whatever their role; `connector:view` and `connector:manage` widen nothing. Connectors the caller's restrictions block are returned, flagged `restricted` and `restricted_by`, not filtered out. Platform catalog rows omit `can_manage` and carry `can_use: true`; on an org-owned row `can_manage` reflects only an `edit` grant of the caller's own. Cursor-paginated: a page holds `limit` rows whenever that many are visible, `prev_cursor` is always null, and the cursor is opaque and valid only for the `search`, `category` and `custom` values it was returned for (anything else is a `400`). A page can occasionally come back empty with a `next_cursor` when many consecutive connectors are hidden from the caller; keep paging until `next_cursor` is null.","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, either from a catalog `config` or — bring your own — from a remote streamable-HTTP MCP server (`remote_mcp`). Send exactly one of the two. 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. A remote MCP connector is private to its creator until shared (the ordinary connector grants), and its tools are discovered per connection, not at creation. `remote_mcp.url` must be https, must not name Elaichi itself, and is immutable afterwards. `auth_mode` says how members sign in: `oauth_auto` (Elaichi registers itself with the server's authorization server — Client ID Metadata Document first, else dynamic registration; 400 when neither exists), `oauth_client` (your own `client_id` and optional `client_secret`), `custom` (an `auth_config` declaring a token form, no OAuth) or `none`. Absent, it is inferred: `client_id` means `oauth_client`, `auth_config` means `custom`, and otherwise the server is probed (OAuth metadata means `oauth_auto`, even when it also answers anonymously; a server with no sign-in at all answers 400 `sign_in_not_offered` unless you send `auth_mode: \"none\"`; anything else answers 400 `remote_mcp_auth_required`). A field that does not belong to the mode is a 400. The OAuth client is stored as the organization's own app for the connector, never in the connector entry. An organization may hold 25 remote MCP connectors (409 `remote_mcp_server_limit`). The 201 is the connector detail, with `kind: \"remote_mcp\"`, `remote_mcp` (`url`, `auth_mode`, `oauth_registration`) and `auth` (what sign-in the connector declares, names only — never a secret).","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["slug","label"],"properties":{"remote_mcp":{"type":"object","required":["url"],"additionalProperties":false,"description":"Mutually exclusive with `config`.","properties":{"url":{"type":"string","maxLength":2048,"description":"The server's streamable-HTTP MCP endpoint (https)."},"auth_mode":{"type":"string","enum":["oauth_auto","oauth_client","custom","none"]},"auth_config":{"type":"object","additionalProperties":true,"description":"`custom` only: `credentials` (an `api_key` form), `authorization`, `headers`, `query` — the shape of a connector config minus `resources`. Values are placeholders (`{{api_key}}`); a literal secret is refused."},"client_id":{"type":"string","maxLength":512,"description":"`oauth_client` only."},"client_secret":{"type":"string","maxLength":2048,"description":"`oauth_client` only. Write-only."},"scopes":{"type":"array","items":{"type":"string"},"description":"OAuth only. Defaults to what the server advertises."},"omit_refresh_scope":{"type":"boolean","description":"OAuth modes only (400 for `custom` and `none`). Off by default. `true` makes a token refresh send no `scope` (RFC 6749 §6: omitted means the original grant); the RFC 8707 `resource` is still sent. For a server that rejects a scope on refresh, such as Close (`400 invalid_scope`). Echoed as `auth.omit_refresh_scope`."}}},"logo":{"type":"string","maxLength":2048,"description":"`remote_mcp` only. An https URL."},"description":{"type":"string","maxLength":2000,"description":"`remote_mcp` only."},"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). Mutually exclusive with `remote_mcp`; `config.remote_mcp` is reserved."}}}}}},"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"},"kind":{"type":"string","enum":["catalog","remote_mcp"],"description":"`remote_mcp` for an organization's own remote MCP connector (`POST /connector` with `remote_mcp`, whose detail also carries `remote_mcp` and `auth`), `catalog` for every other connector. Read it instead of guessing from the slug."},"single_redirect_auth":{"type":"boolean","description":"Hint: exactly one auth method, it can redirect (OAuth), and it has no form fields and no `permissions_text`. `direct_connect_url` on the connect response stays authoritative."},"tool_count":{"type":"integer","description":"The provider's total. For a remote MCP connector that Elaichi publishes, the organization's switched-on tools once anyone in it has connected, and until then the length of the tool list Elaichi published for it (never 0 for a server that publishes tools)."},"connect_notes":{"type":["string","null"],"maxLength":2000,"description":"Remote MCP connectors only: markdown a person should read BEFORE connecting (an admin setting to turn on first, the plan it bills, which region or site to pick). The same text the sign-in page shows above its Connect button (the OAuth config's `permissions_text`). Render it as sanitized markdown. Null when there is none, and for every catalog connector. Detail only."},"connect_notes_link":{"type":["string","null"],"format":"uri","description":"An https guide to that setup (the OAuth config's `documentation_link`). Null when unset."},"connect_blocked":{"type":["object","null"],"required":["reason","can_fix","custom"],"properties":{"reason":{"type":"string","enum":["oauth_app_required"]},"custom":{"type":"boolean","description":"True for an org-owned connector, whose OAuth app is part of its own definition (its Edit screen) rather than an organization override added on the connector page."},"can_fix":{"type":"boolean","description":"Whether THIS caller may add the OAuth app (the same answer as `can_configure_oauth_app`). False means point them at an admin, not at the connector page."}},"description":"Why a connection cannot be started on this connector right now, or null when it can. `oauth_app_required`: `byoa` is true, this organization has not added its own OAuth app, and the connector has no API-key option; `POST /connection` answers 409 `oauth_app_required` for it. Always null for a remote MCP connector. Detail only — the check reads the organization's app, which a list row does not."},"oauth_app":{"type":["object","null"],"required":["needed_for_oauth","app_free_methods"],"properties":{"needed_for_oauth":{"type":"boolean","description":"True when OAuth sign-in on this connector needs an OAuth app this organization has NOT added. False when no app is needed (`byoa` false) or the organization already added its own: `byoa` stays true after that, so it cannot say this."},"app_free_methods":{"type":"array","items":{"type":"string","enum":["api_key","oauth2_client_credentials","jwt_bearer"]},"description":"The auth formats this connector offers that connect without the app. Non-empty with `needed_for_oauth: true` is the mixed case: connectable by these now, and OAuth would first need an admin to add the app. Empty with `needed_for_oauth: true` is `connect_blocked`."}},"description":"Whether signing in with OAuth needs an app this organization has not added, and which other ways in exist. Null means unknown (the lookup failed) — never \"not needed\". Detail only, from the same single read of the organization's app as `connect_blocked`."}}}]}}}},"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 a connector's catalog detail plus `restricted`, which says whether the caller's restrictions block it; a restricted connector still resolves here but cannot be connected or executed. Platform catalog connectors need only `connector:view`. For an org-owned connector the response also carries its config, `owner_user_id` and `owner_member`, the capability fields `can_use`, `can_manage`, `can_share`, `can_transfer`, `can_delete`, `can_revoke_share`, `can_assign_owner` and `can_claim`, a short `access_summary` (counts plus a few names; page `GET /connector/{slug}/share` for the full list), and `shares`, present only when the caller is the owner or has an `edit` grant of their own. A custom connector the caller cannot see is `404`, never `403`.","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":"Updates a custom connector. **`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. A remote MCP connector (`kind: \"remote_mcp\"`) takes a different body: `label`, `category`, `logo`, `description`, `auth_config` (`custom` sign-in only; replaces the declaration whole, and the response carries `reconnect_required: true` when its fields changed so existing connections must sign in again), `client_id` / `client_secret` (`oauth_client` only; an omitted secret keeps the stored one) and `omit_refresh_scope` (OAuth only, else 400 `omit_refresh_scope_not_oauth`; `true` sets it, `false` removes it, read at the next token refresh, so no `reconnect_required`) and `connect_notes` / `connect_notes_link` (OAuth only, else 400 `connect_notes_not_oauth`; `null` clears; stored in place in the OAuth config as `permissions_text` / `documentation_link`, so no `reconnect_required`). Its `url` cannot be changed (400: it would send members' tokens to a different host — create a new connector) and neither can its raw `config` (400).","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. Refused on a remote MCP connector."},"logo":{"type":"string","maxLength":2048,"description":"Remote MCP connectors only. An https URL."},"description":{"type":"string","maxLength":2000,"description":"Remote MCP connectors only."},"auth_config":{"type":"object","additionalProperties":true,"description":"Remote MCP connectors with `custom` sign-in only."},"client_id":{"type":"string","maxLength":512,"description":"Remote MCP connectors with `oauth_client` sign-in only."},"client_secret":{"type":"string","maxLength":2048,"description":"As `client_id`. Write-only."},"omit_refresh_scope":{"type":"boolean","description":"Remote MCP connectors with OAuth sign-in only. `true`: a token refresh sends no scope; `false`: back to the default. Members do not need to reconnect."},"connect_notes":{"type":["string","null"],"minLength":1,"maxLength":2000,"description":"Remote MCP connectors with OAuth sign-in only. Markdown shown before sign-in (trimmed). `null` clears it."},"connect_notes_link":{"type":["string","null"],"maxLength":2048,"format":"uri","description":"As `connect_notes`. An https link with no credentials. `null` clears it."}}}}}},"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":"Permanently deletes an org-owned connector. Refused with 409 while any connection still uses it, so delete or transfer those first. Only the owner may delete: an `edit` grantee gets 403, and a connector with no owner answers 403 `connector_ownerless` to an `edit` grantee until `POST /connector/{slug}/owner` assigns one. The connector's owner record and grants are cleared, so a later connector that reuses the slug inherits neither.","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}/transfer":{"post":{"operationId":"transferConnector","summary":"Transfer connector ownership","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":"Changes the connector's owner. 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). The previous owner held no grant row, so they are left at `use` in the same call (unless they already reach `use` or `edit` another way) — otherwise every connection they made through the connector would be blocked as `connector_not_shared` the moment they gave it away. The new owner can revoke that grant like any other; member offboarding is the exception and leaves a departing owner nothing. The target must be an active member of the organization. Allowed for the current owner only — no `connector:manage` fallback, and an ownerless connector has nobody who may transfer it (403; use `POST /connector/{slug}/owner`). A transfer that races another transfer (or the owner's removal from the org, which deletes the owner row) answers 409 rather than taking the connector back. Recorded in the audit log as `connector.transferred`. The 200 is the connector as the CALLER now sees it, so a previous owner (now a `use` grantee) gets no `owner_user_id`, no `shares` and no manage capabilities back. The audit row carries `previous_owner_kept_use`.","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":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned connector."}]}},"/connector/{slug}/owner":{"post":{"operationId":"assignConnectorOwner","summary":"Assign an owner to an ownerless 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":"Gives a connector that has no owner an owner, and nothing else — every grant is left as it was. Never moves an existing owner: a connector that already has one answers 409 `connector_has_owner` (that is `POST /connector/{slug}/transfer`, owner only), and so does a claim that loses a race with another claim or the creator backfill. Requires `connector:manage`. The target must be an active member of the organization. Naming yourself additionally requires a real `edit` grant of your own (`can_claim`) — a bare `connector:manage` never makes its holder the owner of a connector nobody shared with them (403); naming someone else needs no grant of yours, so a `connector:manage` holder who reaches nothing may assign it. 404 only for a slug that is not an org-owned connector. Recorded in the audit log as `connector.owner_assigned`. The 200 is the connector as the CALLER now sees it.","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":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned connector."}]}},"/connector/{slug}/tools":{"get":{"operationId":"listConnectorTools","summary":"List the tools a connector defines","tags":["Connectors"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor","operation_counts"],"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."},"title":{"type":"string","description":"The upstream server's human title for the tool. Present only on a remote MCP tool that has one."},"tier":{"type":"string","enum":["read","write","destructive"],"description":"Always present. How the approval gate and the MCP scope ladder classify this tool, so a badge shown to a person always matches the gate. For a remote MCP tool it is derived from the server's annotations (unannotated means `destructive`) and follows the server up or down on every refresh. For a platform catalog tool whose method documents MCP annotations, those decide the tier (an HTTP DELETE is always `destructive`); any other catalog tool, including every custom connector and fork tool, takes it from its method and HTTP verb. Never infer a remote MCP tool from it: read `kind`."},"kind":{"type":"string","enum":["catalog","remote_mcp"],"description":"`remote_mcp` for a tool from a remote MCP connector (its `resource` is the constant `mcp`, its `method` the upstream name, and `title` may be set), `catalog` for every other tool."},"tier_source":{"type":"string","enum":["server","default"],"description":"Why a remote MCP tool's `tier` is what it is: `server` (the MCP server's own annotations decided it) or `default` (the server labeled the tool neither way, so the MCP safe default `destructive` applies). Lets a client say \"Not labeled\" rather than show a tier nobody chose. Present only on a remote MCP tool."},"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."},"restricted_scope":{"type":["string","null"],"enum":["connector","tool",null],"description":"`\"connector\"` when the tool is refused because its WHOLE connector is restricted for the caller, `\"tool\"` for a rule about the tool itself, null exactly when `restricted_by` is. Decides which ask can succeed: a tool request on a connector restricted whole is refused (`409 tool_request_connector_blocked`), so ask for the connector. On this listing it is only ever `\"tool\"` when set, since a connector restricted whole 403s the route; toolbox entries carry the same field and can say `\"connector\"`."},"operation":{"type":"string","enum":["read","create","update","delete","other"],"description":"The CRUD bucket this tool falls into, computed from its resolved HTTP verb. Filter with `?operation=`."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]},"operation_counts":{"type":"object","required":["read","create","update","delete","other"],"properties":{"read":{"type":"integer"},"create":{"type":"integer"},"update":{"type":"integer"},"delete":{"type":"integer"},"other":{"type":"integer"}},"description":"How many tools of each CRUD bucket the `q` filter alone leaves — never narrowed further by `operation` itself, so the count beside an unselected filter chip stays honest. Repeated on every page."}}}}}},"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":"Lists every tool a connector defines, each flagged `restricted` and `restricted_by` for the caller. This is the catalog view with no account behind it; use it to pick `tool_name` values for toolbox entries. For what one authenticated account can run, use `GET /connection/{id}/tools`. Blocked tools are flagged, not omitted, so a person can see why one is unavailable (the model-facing `connector.list_tools` operation does omit them). `q` and `operation` are applied before paging, and `operation_counts` describes the `q`-filtered set (never narrowed by `operation` itself), so filter counts stay stable under a search. A restricted connector answers 403; a custom connector the caller cannot see answers 404 (checked first, so the 403 never confirms it exists). Requires `connector:view`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on `name`, `description` and `resource`. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"operation","in":"query","required":false,"description":"Comma-separated CRUD buckets to keep: read, create, update, delete, other. Unknown values 400. Applied before paging, so the cursor never counts a row the filter would have hidden.","schema":{"type":"string"}}]}},"/connector/{slug}/tool":{"get":{"operationId":"listRemoteMcpConnectorTools","summary":"List a remote MCP connector's tool registry","tags":["Custom connectors"],"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"]},"match_count":{"type":"integer","minimum":0,"description":"How many tools match this request's `q` / `tier` / `labeled` (and the caller's `status`), counted on the server. Only on a first page of a narrowed list; absent 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":"Lists the tool registry the organization keeps for one of its own remote MCP connectors: one row per tool name, decided once for every connection of the connector. A tool the server offers is usable at once. Rows carry `status` (`active`, or `disabled` when a manager turned it off), `tier` (derived from the server's annotations and following the server on every refresh), `tier_source` (`server` when the server's annotations decided the tier; `default` when the server sent no `readOnlyHint: true` and no boolean `destructiveHint`, so the safe default `destructive` applies), the full `description` of the latest definition (at most 64,000 characters), `connection_count` (connections that list it now) and `can_manage` (whether the caller may `PATCH` it). A caller with only `use` access sees just the `active` tools, whatever `status` they ask for. Paged by name; `q` is a case-insensitive substring of the name, upstream name, title or description (a space also matches `_`), and `status`, `tier` (one or several) and `labeled` filter too, all before paging. When `q`, `tier` or `labeled` narrows the list, the first page (no `cursor`) also carries `match_count`, the count of every tool matching those filters, to show \"N of M tools\" beside the detail's `remote_mcp.listed_tool_count` (the unfiltered count). Needs `use` access or better on the connector (a `view` grant answers 403; no access, or a connector with no remote MCP server, answers 404) and the `custom_connectors` plan feature. For the catalog capability listing, use `GET /connector/{slug}/tools` instead.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of an org-owned remote MCP connector."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Substring of the name, upstream name, title or description. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"status","in":"query","required":false,"description":"Only tools that are on (`active`) or off (`disabled`). Managers only; a `use` grantee always gets `active`.","schema":{"type":"string","enum":["active","disabled"]}},{"name":"tier","in":"query","required":false,"description":"Only tools whose stored tier is this one, or any of several: a comma list (`tier=read,write`) or the parameter repeated. It is the tier the gate enforces, so `tier=destructive` includes the tools the server left unlabeled (`tier_source: \"default\"`). Pair with `labeled=true` for server labels only. Empty is no filter; any value that is not `read`, `write` or `destructive` is a 400.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["read","write","destructive"]},"examples":[["read"],["read","write"]]}},{"name":"labeled","in":"query","required":false,"description":"`true`: only tools the server labeled itself (`tier_source: \"server\"`); `false`: only the ones it left unlabeled. Empty is no filter; any other value is a 400.","schema":{"type":"string","enum":["true","false"]}}]}},"/connector/{slug}/tool/{name}":{"patch":{"operationId":"updateRemoteMcpConnectorTool","summary":"Turn a remote MCP connector tool off or on","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":"`status: \"disabled\"` switches the tool off for every connection, `\"active\"` turns it back on; nothing else is accepted (400). A tool's tier is always the server's label and cannot be set here. Needs the connector's own edit access (ownership or an `edit` share) on an org's own connector, `connector:manage` on one Elaichi publishes. The 200 is the tool with its whole definition (`description`, `input_schema`, `output_schema`, `annotations`, `derived_tier`). Recorded in the audit log as `connector.tool_updated` with the status before and after.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["status"],"properties":{"status":{"type":"string","enum":["active","disabled"]}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug of a remote MCP connector."},{"name":"name","in":"path","required":true,"schema":{"type":"string"},"description":"The tool's Elaichi name, as `GET /connector/{slug}/tool` returns it."}]}},"/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 and remembers the original as its upstream, so later changes can be pulled. Forking a connector the caller's restrictions block is refused, so a fork cannot be used to get around a restriction. A remote MCP connector (`kind: 'remote_mcp'`) cannot be forked (`409 connector_not_forkable`). 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":"Link a fork to its upstream","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":"Unlink a fork from its upstream","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":"Update connector documentation in bulk","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":"Get this org’s OAuth app 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. Null also when one is stored but held back, see `user_scope_masked`."},"user_scope_masked":{"type":"boolean","description":"True when a `user_scope` is stored encrypted by an older write and cannot be read or changed here. A `PUT` that sets `user_scope` then answers `409`."},"supports_user_scope":{"type":"boolean","description":"Whether this connector’s provider takes user token scopes (Slack does). Where false, a `user_scope` that differs from the stored one is refused with `400`."},"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 — the message says so only to a caller who can see it), for a platform remote MCP connector, and for a connector with no OAuth 2.0 credential (`auth_formats` on `GET /connector/{slug}` does not include `oauth2`) unless this organization already has an override stored for it. Every other catalog OAuth 2.0 connector answers `200`, whether or not Elaichi’s own app already works for it: `oauth_app_mode` is `platform_default` until the organization sets its own, and `byoa: true` on `GET /connector/{slug}` is what says the platform has no usable app and one is required. A client reads this endpoint directly: a `404` here means no organization OAuth application applies to this connector and the override UI should not be offered. 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 app 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. Null also when one is stored but held back, see `user_scope_masked`."},"user_scope_masked":{"type":"boolean","description":"True when a `user_scope` is stored encrypted by an older write and cannot be read or changed here. A `PUT` that sets `user_scope` then answers `409`."},"supports_user_scope":{"type":"boolean","description":"Whether this connector’s provider takes user token scopes (Slack does). Where false, a `user_scope` that differs from the stored one is refused with `400`."},"updated_at":{"type":["string","null"]}}}}}},"400":{"description":"Bad request. Includes a `user_scope` with a token that is not a scope (`validation_error`), and a `user_scope` that differs from the stored one on a connector that takes none (`user_scope_unsupported`).","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":"The stored `user_scope` is held encrypted by an older write and cannot be changed here (`user_scope_masked`). Remove the OAuth application and add it again to replace it.","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":"Authorize-URL scopes (`params.user_scope`), separated by spaces or commas and stored space-separated. Only where `supports_user_scope` is true; omit to clear."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug."}]},"delete":{"operationId":"clearConnectorOAuthApp","summary":"Remove this org’s OAuth app for a connector","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 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":"Lists who a connector is shared with, cursor-paginated in the same shape as `GET /connection/{id}/share` and `GET /toolbox/{id}/share`. Filter with `grantee_type`, and search member names and emails and team names with `q`. Org-owned connectors only. Requires the same access as seeing the shares 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":{"oneOf":[{"$ref":"#/components/schemas/AclEntry"},{"$ref":"#/components/schemas/ShareManyResponse"}]}}}},"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 organization access to an org-owned connector at a level: `view` < `use` < `edit`, the same ladder every shareable resource uses. `use` 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":{"oneOf":[{"$ref":"#/components/schemas/ShareRequest"},{"$ref":"#/components/schemas/ShareManyRequest"}]}}}},"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":"Revokes a share on a connector. 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":"Share id (`acl_…`) 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_…`)."},"restricted_inherited_from":{"type":["object","null"],"properties":{"slug":{"type":["string","null"],"description":"The upstream connector the restriction is inherited from. For API consumers; never render it. Null — together with `label` — when the caller may not be told the upstream (an org-owned upstream they hold no grant on, or one that was deleted): a slug names the connector as surely as its label does."},"label":{"type":["string","null"],"description":"The upstream's display name — null, together with `slug`, when the caller may not be told it. The UI then says \"a connector this was forked from\"."}},"required":["slug","label"],"description":"Set when this connector is restricted for the caller only because the connector it was forked from is. A fork inherits its upstream’s restrictions on purpose (forking a blocked connector is not a bypass), but nothing was set on the fork itself, so this says where the block came from. Null when not restricted, when a rule names this connector directly, or when the upstream could not be traced. Asking for access still targets this connector: approving the fork’s own request opens that fork for that person only (whether the block comes through their role or a rule written about them personally) and the original stays restricted for them; the approved request reads `restriction_lifted: true`. A sibling of `restricted_by`, which stays the precedence layer. Do not derive it yourself from the fork’s upstream."},"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. The owner has implicit `edit`, plus delete and transfer, which 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 Elaichi holds. Written once at creation and never rewritten. It differs from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and does not touch the stored credential, so after a transfer these are two different people and only this one should be shown as \"Connected by\". Null means not recorded, never \"nobody\": connections created before this field existed are null. Show nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id` (name, else email). Present on every response that returns a connection, commands included. A null label beside a non-null `connected_by_user_id` means the person has left the organization; show \"Former member\". Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"connector":{"type":"object","description":"The connector's display data, so a client needs no `GET /connector/{slug}` per distinct connector. On `GET /connection` rows and `GET /connection/{id}` (which adds `can_view_connector`, `available` and `auth_mode`, and always states `single_redirect_auth`); absent from the command responses, which cannot change a connection's connector.","required":["label","logo"],"properties":{"label":{"type":"string","description":"Catalog display name; the slug itself when the catalog cannot resolve it."},"logo":{"type":["string","null"],"description":"Logo URL; null when the connector has none."},"upstream_slug":{"type":"string","description":"Present only on a fork (an org-owned connector copied from another): the connector it was forked from. Omitted when the caller has no access to the fork or to a private upstream (naming it would disclose its slug), and when the upstream cannot be read."},"single_redirect_auth":{"type":"boolean","description":"The connector-row hint of the same name: connecting is most likely a single provider sign-in, so a client reserves a popup inside the Reconnect click. Always stated on `GET /connection/{id}` (false when `can_view_connector` is). On a list row it is stated only where the caller could open the connector, and ABSENT otherwise, which reads as unknown, never false."},"available":{"type":"boolean","description":"False only when the catalog no longer has the connector (Elaichi unpublished or deleted a platform connector): the connection is kept but nothing can work until it returns. On `GET /connection/{id}` it is `true` when the connector resolves; on a `GET /connection` row it is never `true`, only `false` or absent. Absent also when the catalog could not answer, so a brief outage never reads as an unpublished connector."},"auth_mode":{"type":"string","enum":["oauth_auto","oauth_client","custom","none"],"description":"`GET /connection/{id}` only, remote MCP connectors only, and only when `can_view_connector`: how the connector signs in, so a screen can say \"No sign-in\" instead of \"API key\"."},"can_view_connector":{"type":"boolean","description":"`GET /connection/{id}` only. Whether `GET /connector/{slug}` would answer this caller: false for a connector the catalog no longer resolves, without `connector:view`, and for an org-owned connector nobody shared with them (it 404s). Link to the connector only when true."}}},"saffron_account_id":{"type":["string","null"],"description":"Identifier of the stored credential Elaichi holds for this connection. Opaque; 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 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. 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","description":"Who `owner_user_id` is. Present on a connection the caller does not own and absent on their own. On every response that returns a connection (list, detail and the command responses), so a transfer names the new owner without a reload. `name` and `email` are absent for an owner who has left the organization (\"Former member\").","required":["id"],"properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":"string","description":"Display name, else the email."},"email":{"type":"string","format":"email"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"shares":{"type":"array","description":"Present only when the caller may see who the connection is shared with: the owner or `edit` access. A `view` or `use` recipient is never told who else it 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: the owner, `edit` access, or a team admin on a team-granted one, and `connection:share`. Computed by the API, so read it rather than re-deriving it from `owner_user_id` and permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings: the owner, or `edit` access."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection: the owner only. Narrower than `can_manage`, because an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may grant or revoke access to this connection. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `connection:share` can reconfigure or reconnect the connection but cannot change who else has access. Mirrors `DELETE /connection/{id}/share/{aclId}`."},"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. A short summary of who has access; 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the connection reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Lists the connections the caller owns plus every connection shared with them, directly, through a team they belong to, or organization-wide. No organization permission widens this: a private connection (no grants) appears only for its owner, whatever permission an org owner or admin holds, and there is no org-wide listing of connections. Statuses are refreshed as they are read, so this is also how a `needs_reauth` connection surfaces. A `pending` row whose sign-in finished but whose redirect never came back is bound shortly after the response, so it may still read `pending` and show as bound one load later. Rows the caller may share (owner or `edit` access) also carry `can_share: true` and a short `access_summary`; 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","direct_connect_url"],"properties":{"connection":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (`conn_…`)."},"restricted_inherited_from":{"type":["object","null"],"properties":{"slug":{"type":["string","null"],"description":"The upstream connector the restriction is inherited from. For API consumers; never render it. Null — together with `label` — when the caller may not be told the upstream (an org-owned upstream they hold no grant on, or one that was deleted): a slug names the connector as surely as its label does."},"label":{"type":["string","null"],"description":"The upstream's display name — null, together with `slug`, when the caller may not be told it. The UI then says \"a connector this was forked from\"."}},"required":["slug","label"],"description":"Set when this connector is restricted for the caller only because the connector it was forked from is. A fork inherits its upstream’s restrictions on purpose (forking a blocked connector is not a bypass), but nothing was set on the fork itself, so this says where the block came from. Null when not restricted, when a rule names this connector directly, or when the upstream could not be traced. Asking for access still targets this connector: approving the fork’s own request opens that fork for that person only (whether the block comes through their role or a rule written about them personally) and the original stays restricted for them; the approved request reads `restriction_lifted: true`. A sibling of `restricted_by`, which stays the precedence layer. Do not derive it yourself from the fork’s upstream."},"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. The owner has implicit `edit`, plus delete and transfer, which 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 Elaichi holds. Written once at creation and never rewritten. It differs from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and does not touch the stored credential, so after a transfer these are two different people and only this one should be shown as \"Connected by\". Null means not recorded, never \"nobody\": connections created before this field existed are null. Show nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id` (name, else email). Present on every response that returns a connection, commands included. A null label beside a non-null `connected_by_user_id` means the person has left the organization; show \"Former member\". Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"connector":{"type":"object","description":"The connector's display data, so a client needs no `GET /connector/{slug}` per distinct connector. On `GET /connection` rows and `GET /connection/{id}` (which adds `can_view_connector`, `available` and `auth_mode`, and always states `single_redirect_auth`); absent from the command responses, which cannot change a connection's connector.","required":["label","logo"],"properties":{"label":{"type":"string","description":"Catalog display name; the slug itself when the catalog cannot resolve it."},"logo":{"type":["string","null"],"description":"Logo URL; null when the connector has none."},"upstream_slug":{"type":"string","description":"Present only on a fork (an org-owned connector copied from another): the connector it was forked from. Omitted when the caller has no access to the fork or to a private upstream (naming it would disclose its slug), and when the upstream cannot be read."},"single_redirect_auth":{"type":"boolean","description":"The connector-row hint of the same name: connecting is most likely a single provider sign-in, so a client reserves a popup inside the Reconnect click. Always stated on `GET /connection/{id}` (false when `can_view_connector` is). On a list row it is stated only where the caller could open the connector, and ABSENT otherwise, which reads as unknown, never false."},"available":{"type":"boolean","description":"False only when the catalog no longer has the connector (Elaichi unpublished or deleted a platform connector): the connection is kept but nothing can work until it returns. On `GET /connection/{id}` it is `true` when the connector resolves; on a `GET /connection` row it is never `true`, only `false` or absent. Absent also when the catalog could not answer, so a brief outage never reads as an unpublished connector."},"auth_mode":{"type":"string","enum":["oauth_auto","oauth_client","custom","none"],"description":"`GET /connection/{id}` only, remote MCP connectors only, and only when `can_view_connector`: how the connector signs in, so a screen can say \"No sign-in\" instead of \"API key\"."},"can_view_connector":{"type":"boolean","description":"`GET /connection/{id}` only. Whether `GET /connector/{slug}` would answer this caller: false for a connector the catalog no longer resolves, without `connector:view`, and for an org-owned connector nobody shared with them (it 404s). Link to the connector only when true."}}},"saffron_account_id":{"type":["string","null"],"description":"Identifier of the stored credential Elaichi holds for this connection. Opaque; 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 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. 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","description":"Who `owner_user_id` is. Present on a connection the caller does not own and absent on their own. On every response that returns a connection (list, detail and the command responses), so a transfer names the new owner without a reload. `name` and `email` are absent for an owner who has left the organization (\"Former member\").","required":["id"],"properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":"string","description":"Display name, else the email."},"email":{"type":"string","format":"email"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"shares":{"type":"array","description":"Present only when the caller may see who the connection is shared with: the owner or `edit` access. A `view` or `use` recipient is never told who else it 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: the owner, `edit` access, or a team admin on a team-granted one, and `connection:share`. Computed by the API, so read it rather than re-deriving it from `owner_user_id` and permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings: the owner, or `edit` access."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection: the owner only. Narrower than `can_manage`, because an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may grant or revoke access to this connection. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `connection:share` can reconfigure or reconnect the connection but cannot change who else has access. Mirrors `DELETE /connection/{id}/share/{aclId}`."},"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. A short summary of who has access; 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the connection reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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 person 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. When they finish, the provider returns them to Elaichi and the status becomes `active`."},"direct_connect_url":{"type":["string","null"],"format":"uri","description":"The same one-time session as `connect_url`, but it goes straight to the provider's sign-in instead of the hosted connect page, so the person skips the page. Non-null only when the connector has exactly one auth method, that method is a redirect (OAuth), and it has no form fields or `permissions_text`; those are the cases where the hosted page would show nothing but a Connect button. Open it top-level in a popup or a new tab, never in an iframe. `null` means use `connect_url`; so does a lookup that failed or took longer than 1.5 seconds."}}}}}},"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: 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. Connecting through an org-owned connector nobody shared with the caller answers 403 `connector_not_shared` (read `can_use` on the connector row to know in advance); a connector an admin restriction blocks is a different 403, flagged `restricted` on the connector. A connector that needs the organization's own OAuth app (`byoa: true`), has none added yet and offers no API-key option answers 409 `oauth_app_required` before anything is written, so no `pending` connection is left behind. `details` carries `connector_slug`, `can_configure_oauth_app` and `setup_path` (`/connectors/{slug}`, the connector page where the app is added; null when the caller may not add one, so tell them to ask an admin). Read `connect_blocked` on `GET /connector/{slug}` to know in advance.","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_…`)."},"restricted_inherited_from":{"type":["object","null"],"properties":{"slug":{"type":["string","null"],"description":"The upstream connector the restriction is inherited from. For API consumers; never render it. Null — together with `label` — when the caller may not be told the upstream (an org-owned upstream they hold no grant on, or one that was deleted): a slug names the connector as surely as its label does."},"label":{"type":["string","null"],"description":"The upstream's display name — null, together with `slug`, when the caller may not be told it. The UI then says \"a connector this was forked from\"."}},"required":["slug","label"],"description":"Set when this connector is restricted for the caller only because the connector it was forked from is. A fork inherits its upstream’s restrictions on purpose (forking a blocked connector is not a bypass), but nothing was set on the fork itself, so this says where the block came from. Null when not restricted, when a rule names this connector directly, or when the upstream could not be traced. Asking for access still targets this connector: approving the fork’s own request opens that fork for that person only (whether the block comes through their role or a rule written about them personally) and the original stays restricted for them; the approved request reads `restriction_lifted: true`. A sibling of `restricted_by`, which stays the precedence layer. Do not derive it yourself from the fork’s upstream."},"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. The owner has implicit `edit`, plus delete and transfer, which 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 Elaichi holds. Written once at creation and never rewritten. It differs from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and does not touch the stored credential, so after a transfer these are two different people and only this one should be shown as \"Connected by\". Null means not recorded, never \"nobody\": connections created before this field existed are null. Show nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id` (name, else email). Present on every response that returns a connection, commands included. A null label beside a non-null `connected_by_user_id` means the person has left the organization; show \"Former member\". Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"connector":{"type":"object","description":"The connector's display data, so a client needs no `GET /connector/{slug}` per distinct connector. On `GET /connection` rows and `GET /connection/{id}` (which adds `can_view_connector`, `available` and `auth_mode`, and always states `single_redirect_auth`); absent from the command responses, which cannot change a connection's connector.","required":["label","logo"],"properties":{"label":{"type":"string","description":"Catalog display name; the slug itself when the catalog cannot resolve it."},"logo":{"type":["string","null"],"description":"Logo URL; null when the connector has none."},"upstream_slug":{"type":"string","description":"Present only on a fork (an org-owned connector copied from another): the connector it was forked from. Omitted when the caller has no access to the fork or to a private upstream (naming it would disclose its slug), and when the upstream cannot be read."},"single_redirect_auth":{"type":"boolean","description":"The connector-row hint of the same name: connecting is most likely a single provider sign-in, so a client reserves a popup inside the Reconnect click. Always stated on `GET /connection/{id}` (false when `can_view_connector` is). On a list row it is stated only where the caller could open the connector, and ABSENT otherwise, which reads as unknown, never false."},"available":{"type":"boolean","description":"False only when the catalog no longer has the connector (Elaichi unpublished or deleted a platform connector): the connection is kept but nothing can work until it returns. On `GET /connection/{id}` it is `true` when the connector resolves; on a `GET /connection` row it is never `true`, only `false` or absent. Absent also when the catalog could not answer, so a brief outage never reads as an unpublished connector."},"auth_mode":{"type":"string","enum":["oauth_auto","oauth_client","custom","none"],"description":"`GET /connection/{id}` only, remote MCP connectors only, and only when `can_view_connector`: how the connector signs in, so a screen can say \"No sign-in\" instead of \"API key\"."},"can_view_connector":{"type":"boolean","description":"`GET /connection/{id}` only. Whether `GET /connector/{slug}` would answer this caller: false for a connector the catalog no longer resolves, without `connector:view`, and for an org-owned connector nobody shared with them (it 404s). Link to the connector only when true."}}},"saffron_account_id":{"type":["string","null"],"description":"Identifier of the stored credential Elaichi holds for this connection. Opaque; 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 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. 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","description":"Who `owner_user_id` is. Present on a connection the caller does not own and absent on their own. On every response that returns a connection (list, detail and the command responses), so a transfer names the new owner without a reload. `name` and `email` are absent for an owner who has left the organization (\"Former member\").","required":["id"],"properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":"string","description":"Display name, else the email."},"email":{"type":"string","format":"email"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"shares":{"type":"array","description":"Present only when the caller may see who the connection is shared with: the owner or `edit` access. A `view` or `use` recipient is never told who else it 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: the owner, `edit` access, or a team admin on a team-granted one, and `connection:share`. Computed by the API, so read it rather than re-deriving it from `owner_user_id` and permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings: the owner, or `edit` access."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection: the owner only. Narrower than `can_manage`, because an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may grant or revoke access to this connection. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `connection:share` can reconfigure or reconnect the connection but cannot change who else has access. Mirrors `DELETE /connection/{id}/share/{aclId}`."},"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. A short summary of who has access; 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the connection reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Returns one connection. Status is refreshed on read. Connections the caller cannot see answer 404, never 403, including another member's private connection: the caller must own the connection or hold a grant on it (directly, through a team, or org-wide), even if they hold `connection:manage`. `can_share`, `can_manage` and `can_transfer` say whether the caller may share, edit, or transfer or delete the connection; only ownership or a grant reaches them (see the schema for each). `access_summary`, a short summary of who has access, 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_…`)."},"restricted_inherited_from":{"type":["object","null"],"properties":{"slug":{"type":["string","null"],"description":"The upstream connector the restriction is inherited from. For API consumers; never render it. Null — together with `label` — when the caller may not be told the upstream (an org-owned upstream they hold no grant on, or one that was deleted): a slug names the connector as surely as its label does."},"label":{"type":["string","null"],"description":"The upstream's display name — null, together with `slug`, when the caller may not be told it. The UI then says \"a connector this was forked from\"."}},"required":["slug","label"],"description":"Set when this connector is restricted for the caller only because the connector it was forked from is. A fork inherits its upstream’s restrictions on purpose (forking a blocked connector is not a bypass), but nothing was set on the fork itself, so this says where the block came from. Null when not restricted, when a rule names this connector directly, or when the upstream could not be traced. Asking for access still targets this connector: approving the fork’s own request opens that fork for that person only (whether the block comes through their role or a rule written about them personally) and the original stays restricted for them; the approved request reads `restriction_lifted: true`. A sibling of `restricted_by`, which stays the precedence layer. Do not derive it yourself from the fork’s upstream."},"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. The owner has implicit `edit`, plus delete and transfer, which 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 Elaichi holds. Written once at creation and never rewritten. It differs from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and does not touch the stored credential, so after a transfer these are two different people and only this one should be shown as \"Connected by\". Null means not recorded, never \"nobody\": connections created before this field existed are null. Show nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id` (name, else email). Present on every response that returns a connection, commands included. A null label beside a non-null `connected_by_user_id` means the person has left the organization; show \"Former member\". Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"connector":{"type":"object","description":"The connector's display data, so a client needs no `GET /connector/{slug}` per distinct connector. On `GET /connection` rows and `GET /connection/{id}` (which adds `can_view_connector`, `available` and `auth_mode`, and always states `single_redirect_auth`); absent from the command responses, which cannot change a connection's connector.","required":["label","logo"],"properties":{"label":{"type":"string","description":"Catalog display name; the slug itself when the catalog cannot resolve it."},"logo":{"type":["string","null"],"description":"Logo URL; null when the connector has none."},"upstream_slug":{"type":"string","description":"Present only on a fork (an org-owned connector copied from another): the connector it was forked from. Omitted when the caller has no access to the fork or to a private upstream (naming it would disclose its slug), and when the upstream cannot be read."},"single_redirect_auth":{"type":"boolean","description":"The connector-row hint of the same name: connecting is most likely a single provider sign-in, so a client reserves a popup inside the Reconnect click. Always stated on `GET /connection/{id}` (false when `can_view_connector` is). On a list row it is stated only where the caller could open the connector, and ABSENT otherwise, which reads as unknown, never false."},"available":{"type":"boolean","description":"False only when the catalog no longer has the connector (Elaichi unpublished or deleted a platform connector): the connection is kept but nothing can work until it returns. On `GET /connection/{id}` it is `true` when the connector resolves; on a `GET /connection` row it is never `true`, only `false` or absent. Absent also when the catalog could not answer, so a brief outage never reads as an unpublished connector."},"auth_mode":{"type":"string","enum":["oauth_auto","oauth_client","custom","none"],"description":"`GET /connection/{id}` only, remote MCP connectors only, and only when `can_view_connector`: how the connector signs in, so a screen can say \"No sign-in\" instead of \"API key\"."},"can_view_connector":{"type":"boolean","description":"`GET /connection/{id}` only. Whether `GET /connector/{slug}` would answer this caller: false for a connector the catalog no longer resolves, without `connector:view`, and for an org-owned connector nobody shared with them (it 404s). Link to the connector only when true."}}},"saffron_account_id":{"type":["string","null"],"description":"Identifier of the stored credential Elaichi holds for this connection. Opaque; 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 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. 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","description":"Who `owner_user_id` is. Present on a connection the caller does not own and absent on their own. On every response that returns a connection (list, detail and the command responses), so a transfer names the new owner without a reload. `name` and `email` are absent for an owner who has left the organization (\"Former member\").","required":["id"],"properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":"string","description":"Display name, else the email."},"email":{"type":"string","format":"email"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"shares":{"type":"array","description":"Present only when the caller may see who the connection is shared with: the owner or `edit` access. A `view` or `use` recipient is never told who else it 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: the owner, `edit` access, or a team admin on a team-granted one, and `connection:share`. Computed by the API, so read it rather than re-deriving it from `owner_user_id` and permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings: the owner, or `edit` access."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection: the owner only. Narrower than `can_manage`, because an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may grant or revoke access to this connection. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `connection:share` can reconfigure or reconnect the connection but cannot change who else has access. Mirrors `DELETE /connection/{id}/share/{aclId}`."},"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. A short summary of who has access; 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the connection reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Renames a connection. 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":"Permanently deletes the connection and its stored credential. 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 the connection stop resolving, and every grant on it is discarded. To hand the connection to someone else instead, use `POST /connection/{id}/transfer`. Owner only; an `edit` share does not reach delete.","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","direct_connect_url"],"properties":{"connection":{"type":"object","properties":{"id":{"type":"string","description":"Connection id (`conn_…`)."},"restricted_inherited_from":{"type":["object","null"],"properties":{"slug":{"type":["string","null"],"description":"The upstream connector the restriction is inherited from. For API consumers; never render it. Null — together with `label` — when the caller may not be told the upstream (an org-owned upstream they hold no grant on, or one that was deleted): a slug names the connector as surely as its label does."},"label":{"type":["string","null"],"description":"The upstream's display name — null, together with `slug`, when the caller may not be told it. The UI then says \"a connector this was forked from\"."}},"required":["slug","label"],"description":"Set when this connector is restricted for the caller only because the connector it was forked from is. A fork inherits its upstream’s restrictions on purpose (forking a blocked connector is not a bypass), but nothing was set on the fork itself, so this says where the block came from. Null when not restricted, when a rule names this connector directly, or when the upstream could not be traced. Asking for access still targets this connector: approving the fork’s own request opens that fork for that person only (whether the block comes through their role or a rule written about them personally) and the original stays restricted for them; the approved request reads `restriction_lifted: true`. A sibling of `restricted_by`, which stays the precedence layer. Do not derive it yourself from the fork’s upstream."},"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. The owner has implicit `edit`, plus delete and transfer, which 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 Elaichi holds. Written once at creation and never rewritten. It differs from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and does not touch the stored credential, so after a transfer these are two different people and only this one should be shown as \"Connected by\". Null means not recorded, never \"nobody\": connections created before this field existed are null. Show nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id` (name, else email). Present on every response that returns a connection, commands included. A null label beside a non-null `connected_by_user_id` means the person has left the organization; show \"Former member\". Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"connector":{"type":"object","description":"The connector's display data, so a client needs no `GET /connector/{slug}` per distinct connector. On `GET /connection` rows and `GET /connection/{id}` (which adds `can_view_connector`, `available` and `auth_mode`, and always states `single_redirect_auth`); absent from the command responses, which cannot change a connection's connector.","required":["label","logo"],"properties":{"label":{"type":"string","description":"Catalog display name; the slug itself when the catalog cannot resolve it."},"logo":{"type":["string","null"],"description":"Logo URL; null when the connector has none."},"upstream_slug":{"type":"string","description":"Present only on a fork (an org-owned connector copied from another): the connector it was forked from. Omitted when the caller has no access to the fork or to a private upstream (naming it would disclose its slug), and when the upstream cannot be read."},"single_redirect_auth":{"type":"boolean","description":"The connector-row hint of the same name: connecting is most likely a single provider sign-in, so a client reserves a popup inside the Reconnect click. Always stated on `GET /connection/{id}` (false when `can_view_connector` is). On a list row it is stated only where the caller could open the connector, and ABSENT otherwise, which reads as unknown, never false."},"available":{"type":"boolean","description":"False only when the catalog no longer has the connector (Elaichi unpublished or deleted a platform connector): the connection is kept but nothing can work until it returns. On `GET /connection/{id}` it is `true` when the connector resolves; on a `GET /connection` row it is never `true`, only `false` or absent. Absent also when the catalog could not answer, so a brief outage never reads as an unpublished connector."},"auth_mode":{"type":"string","enum":["oauth_auto","oauth_client","custom","none"],"description":"`GET /connection/{id}` only, remote MCP connectors only, and only when `can_view_connector`: how the connector signs in, so a screen can say \"No sign-in\" instead of \"API key\"."},"can_view_connector":{"type":"boolean","description":"`GET /connection/{id}` only. Whether `GET /connector/{slug}` would answer this caller: false for a connector the catalog no longer resolves, without `connector:view`, and for an org-owned connector nobody shared with them (it 404s). Link to the connector only when true."}}},"saffron_account_id":{"type":["string","null"],"description":"Identifier of the stored credential Elaichi holds for this connection. Opaque; 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 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. 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","description":"Who `owner_user_id` is. Present on a connection the caller does not own and absent on their own. On every response that returns a connection (list, detail and the command responses), so a transfer names the new owner without a reload. `name` and `email` are absent for an owner who has left the organization (\"Former member\").","required":["id"],"properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":"string","description":"Display name, else the email."},"email":{"type":"string","format":"email"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"shares":{"type":"array","description":"Present only when the caller may see who the connection is shared with: the owner or `edit` access. A `view` or `use` recipient is never told who else it 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: the owner, `edit` access, or a team admin on a team-granted one, and `connection:share`. Computed by the API, so read it rather than re-deriving it from `owner_user_id` and permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings: the owner, or `edit` access."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection: the owner only. Narrower than `can_manage`, because an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may grant or revoke access to this connection. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `connection:share` can reconfigure or reconnect the connection but cannot change who else has access. Mirrors `DELETE /connection/{id}/share/{aclId}`."},"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. A short summary of who has access; 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the connection reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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 person 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. When they finish, the provider returns them to Elaichi and the status becomes `active`."},"direct_connect_url":{"type":["string","null"],"format":"uri","description":"The same one-time session as `connect_url`, but it goes straight to the provider's sign-in instead of the hosted connect page, so the person skips the page. Non-null only when the connector has exactly one auth method, that method is a redirect (OAuth), and it has no form fields or `permissions_text`; those are the cases where the hosted page would show nothing but a Connect button. Open it top-level in a popup or a new tab, never in an iframe. `null` means use `connect_url`; so does a lookup that failed or took longer than 1.5 seconds."}}}}}},"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; this is 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 and the old stored credential is dropped. The body is optional: an empty POST returns the person to the app root. On an org-owned connector it needs the same `use` level as `POST /connection` (403 `connector_not_shared` once the share was withdrawn). `409 reconnect_not_needed` when the connector is a remote MCP server with no sign-in (auth mode `none`) and the connection is `active`: there is nothing to reconnect, and the row's `can_reconnect` is `false` for the same reason. A `pending` no-sign-in connection (tab closed before the callback) still reconnects; it is the only way to a fresh session.","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 Elaichi to mint a fresh access token for this connection in place. Use it for maintenance on a connection that is still healthy; reconnect is the repair for one that is not. No body. Same permission as reconnect, and only meaningful on a bound, `active` row whose `authentication_method` is refreshable (`oauth2`, `oauth2_client_credentials`, `jwt_bearer`), which is what the row's `can_refresh_credentials` reports. Everything else answers `409`: `not_refreshable` (no provider call is made), `needs_reauth` (the provider rejected the refresh; the row is set to `needs_reauth` before this reply), or `connection_disconnected`. The `200` is a receipt, never the credential itself.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Connection id (`conn_…`)."}]}},"/connection/{id}/refresh-tools":{"post":{"operationId":"refreshConnectionTools","summary":"Refresh a remote MCP connection’s tools","tags":["Connections"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["listed","summary","tools_refreshed_at","tools_refresh_error"],"properties":{"listed":{"type":"integer","description":"Tools this connection lists now."},"summary":{"type":"object","required":["added","changed","removed","tier_changed","tier_changes"],"properties":{"added":{"type":"array","items":{"type":"string"},"description":"Names new to the connector’s registry."},"changed":{"type":"array","items":{"type":"string"},"description":"Names whose definition changed."},"removed":{"type":"array","items":{"type":"string"},"description":"Names this connection stopped listing."},"tier_changed":{"type":"array","items":{"type":"string"},"description":"Names whose tier on this connection's own definition changed, in either direction."},"tier_changes":{"type":"array","description":"The `tier_changed` tools with the direction each moved (`from` this connection's previous definition's tier, `to` its new one's).","items":{"type":"object","required":["name","from","to"],"properties":{"name":{"type":"string"},"from":{"type":"string","enum":["read","write","destructive"]},"to":{"type":"string","enum":["read","write","destructive"]}}}}}},"tools_refreshed_at":{"type":"string","format":"date-time","nullable":true},"tools_refresh_error":{"type":"string","nullable":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":"Lists the tools the remote MCP server shows THIS connection (with its own credential) again, now, and applies the result to the connector’s registry; a tool new to it is usable at once. A remote MCP connection also refreshes when it becomes active and once a day; this is the on-demand one. No body. Same mutation bar as reconnect (`can_manage`); render controls on the connection’s `can_refresh_tools`, which is present only on a remote MCP connection. Rate limited to one per connection per minute: `429` with `Retry-After`. The `200` carries the diff as tool NAMES (`summary.added`, `changed`, `removed`, `tier_changed`, and `tier_changes` with each tier's direction), never definitions. `403` `connector_not_shared` when the connector's owner stopped sharing it; `409`: `not_remote_mcp`, `not_refreshable` (not active or not bound), `needs_reauth` (the server rejected the credential; the connection is moved to `needs_reauth`), or `tools_refresh_failed` (the message is safe to show and is also on the row as `tools_refresh_error`).","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. Use it to repair `post_install_error`, or to re-apply setup after a variable was corrected. No body. Same permission as reconnect; the detail read's `can_run_post_install` says whether to offer it. Answers `202 { \"started\": true }`, never `200`: the steps run in the background, so the real outcome lands on the row (`status` and `last_error`) and shows on a later read. Re-fetch on the user's own refresh rather than sleeping and treating the `202` as the result. `409` when the connector declares no steps (`no_post_install`), the connection is unbound (`not_bound`), it is already fully set up (`already_active`; this route repairs `post_install_error`, not a healthy connection), its sign-in needs to happen again (`needs_reauth`), or Elaichi 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":"Whether Elaichi stores this value as a secret; the value itself is returned 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":"Lists 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 Elaichi stores as a secret. Requires `can_manage`, not just read access: a `view` or `use` grantee runs tools through the connection, but the answers its owner typed into the connect form are not part of that, so a caller without it gets `403`, deliberately not an empty `200`. An unbound connection answers an empty list. `can_manage` here means the owner or a holder of an `edit` grant.","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":"Whether Elaichi stores this value as a secret; the value itself is returned 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":"Updates a connection's context variables. 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","operation_counts"],"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."},"title":{"type":"string","description":"The upstream server's human title for the tool. Present only on a remote MCP tool that has one."},"tier":{"type":"string","enum":["read","write","destructive"],"description":"Always present. How the approval gate and the MCP scope ladder classify this tool, so a badge shown to a person always matches the gate. For a remote MCP tool it is derived from the server's annotations (unannotated means `destructive`) and follows the server up or down on every refresh. For a platform catalog tool whose method documents MCP annotations, those decide the tier (an HTTP DELETE is always `destructive`); any other catalog tool, including every custom connector and fork tool, takes it from its method and HTTP verb. Never infer a remote MCP tool from it: read `kind`."},"kind":{"type":"string","enum":["catalog","remote_mcp"],"description":"`remote_mcp` for a tool from a remote MCP connector (its `resource` is the constant `mcp`, its `method` the upstream name, and `title` may be set), `catalog` for every other tool."},"tier_source":{"type":"string","enum":["server","default"],"description":"Why a remote MCP tool's `tier` is what it is: `server` (the MCP server's own annotations decided it) or `default` (the server labeled the tool neither way, so the MCP safe default `destructive` applies). Lets a client say \"Not labeled\" rather than show a tier nobody chose. Present only on a remote MCP tool."},"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."},"restricted_scope":{"type":["string","null"],"enum":["connector","tool",null],"description":"`\"connector\"` when the tool is refused because its WHOLE connector is restricted for the caller, `\"tool\"` for a rule about the tool itself, null exactly when `restricted_by` is. Decides which ask can succeed: a tool request on a connector restricted whole is refused (`409 tool_request_connector_blocked`), so ask for the connector. On this listing it is only ever `\"tool\"` when set, since a connector restricted whole 403s the route; toolbox entries carry the same field and can say `\"connector\"`."},"operation":{"type":"string","enum":["read","create","update","delete","other"],"description":"The CRUD bucket this tool falls into, computed from its resolved HTTP verb. Filter with `?operation=`."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]},"operation_counts":{"type":"object","required":["read","create","update","delete","other"],"properties":{"read":{"type":"integer"},"create":{"type":"integer"},"update":{"type":"integer"},"delete":{"type":"integer"},"other":{"type":"integer"}},"description":"How many tools of each CRUD bucket the `q` filter alone leaves — never narrowed further by `operation` itself, so the count beside an unselected filter chip stays honest. Repeated on every page."}}}}}},"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":"Lists what this authenticated account can run: the connector's tools, each flagged `restricted` and `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. It resolves the same access as `GET /connection/{id}`: a connection the caller holds no grant on is `404`. `q` and `operation` are both applied before paging, and `operation_counts` describes the `q`-filtered set (never narrowed by `operation` itself), same as `GET /connector/{slug}/tools`. For a remote MCP connection (\"bring your own\") the rows are the tools this connection's own credentials listed, from per-connection discovery: `name`, `upstream_name`, `title`, the full `description` of this connection's definition, `status` (`active` or `disabled`, the connector-wide on/off), the `tier` (the server's label), `tier_source` (`server` or `default`), `operation` (the tier as `read`, `other` or `delete`) and `last_seen_at`, plus the same restriction flags. `q` and `status` are applied before paging. A caller who manages the connection may filter by either `status` and sees tools that are turned off; everyone else with access is shown `active` tools only, whatever `status` they send. When the connection's connector is gone from the catalog (Elaichi unpublished or deleted it) the answer is `409 connector_unavailable` with a message a person can read; the connection itself is kept.","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":"Case-insensitive substring match on `name`, `description` and `resource`. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"operation","in":"query","required":false,"description":"Comma-separated CRUD buckets to keep: read, create, update, delete, other. Unknown values 400. Applied before paging, so the cursor never counts a row the filter would have hidden.","schema":{"type":"string"}}]}},"/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_…`)."},"restricted_inherited_from":{"type":["object","null"],"properties":{"slug":{"type":["string","null"],"description":"The upstream connector the restriction is inherited from. For API consumers; never render it. Null — together with `label` — when the caller may not be told the upstream (an org-owned upstream they hold no grant on, or one that was deleted): a slug names the connector as surely as its label does."},"label":{"type":["string","null"],"description":"The upstream's display name — null, together with `slug`, when the caller may not be told it. The UI then says \"a connector this was forked from\"."}},"required":["slug","label"],"description":"Set when this connector is restricted for the caller only because the connector it was forked from is. A fork inherits its upstream’s restrictions on purpose (forking a blocked connector is not a bypass), but nothing was set on the fork itself, so this says where the block came from. Null when not restricted, when a rule names this connector directly, or when the upstream could not be traced. Asking for access still targets this connector: approving the fork’s own request opens that fork for that person only (whether the block comes through their role or a rule written about them personally) and the original stays restricted for them; the approved request reads `restriction_lifted: true`. A sibling of `restricted_by`, which stays the precedence layer. Do not derive it yourself from the fork’s upstream."},"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. The owner has implicit `edit`, plus delete and transfer, which 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 Elaichi holds. Written once at creation and never rewritten. It differs from `owner_user_id` on purpose: `POST /connection/{id}/transfer` moves the owner and does not touch the stored credential, so after a transfer these are two different people and only this one should be shown as \"Connected by\". Null means not recorded, never \"nobody\": connections created before this field existed are null. Show nothing for a null."},"connected_by_label":{"type":["string","null"],"description":"Display name for `connected_by_user_id` (name, else email). Present on every response that returns a connection, commands included. A null label beside a non-null `connected_by_user_id` means the person has left the organization; show \"Former member\". Compare the two fields, never one alone: a null id means \"not recorded\", a null label means \"departed\"."},"connector":{"type":"object","description":"The connector's display data, so a client needs no `GET /connector/{slug}` per distinct connector. On `GET /connection` rows and `GET /connection/{id}` (which adds `can_view_connector`, `available` and `auth_mode`, and always states `single_redirect_auth`); absent from the command responses, which cannot change a connection's connector.","required":["label","logo"],"properties":{"label":{"type":"string","description":"Catalog display name; the slug itself when the catalog cannot resolve it."},"logo":{"type":["string","null"],"description":"Logo URL; null when the connector has none."},"upstream_slug":{"type":"string","description":"Present only on a fork (an org-owned connector copied from another): the connector it was forked from. Omitted when the caller has no access to the fork or to a private upstream (naming it would disclose its slug), and when the upstream cannot be read."},"single_redirect_auth":{"type":"boolean","description":"The connector-row hint of the same name: connecting is most likely a single provider sign-in, so a client reserves a popup inside the Reconnect click. Always stated on `GET /connection/{id}` (false when `can_view_connector` is). On a list row it is stated only where the caller could open the connector, and ABSENT otherwise, which reads as unknown, never false."},"available":{"type":"boolean","description":"False only when the catalog no longer has the connector (Elaichi unpublished or deleted a platform connector): the connection is kept but nothing can work until it returns. On `GET /connection/{id}` it is `true` when the connector resolves; on a `GET /connection` row it is never `true`, only `false` or absent. Absent also when the catalog could not answer, so a brief outage never reads as an unpublished connector."},"auth_mode":{"type":"string","enum":["oauth_auto","oauth_client","custom","none"],"description":"`GET /connection/{id}` only, remote MCP connectors only, and only when `can_view_connector`: how the connector signs in, so a screen can say \"No sign-in\" instead of \"API key\"."},"can_view_connector":{"type":"boolean","description":"`GET /connection/{id}` only. Whether `GET /connector/{slug}` would answer this caller: false for a connector the catalog no longer resolves, without `connector:view`, and for an org-owned connector nobody shared with them (it 404s). Link to the connector only when true."}}},"saffron_account_id":{"type":["string","null"],"description":"Identifier of the stored credential Elaichi holds for this connection. Opaque; 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 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. 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","description":"Who `owner_user_id` is. Present on a connection the caller does not own and absent on their own. On every response that returns a connection (list, detail and the command responses), so a transfer names the new owner without a reload. `name` and `email` are absent for an owner who has left the organization (\"Former member\").","required":["id"],"properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `owner_user_id`."},"name":{"type":"string","description":"Display name, else the email."},"email":{"type":"string","format":"email"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"shares":{"type":"array","description":"Present only when the caller may see who the connection is shared with: the owner or `edit` access. A `view` or `use` recipient is never told who else it 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: the owner, `edit` access, or a team admin on a team-granted one, and `connection:share`. Computed by the API, so read it rather than re-deriving it from `owner_user_id` and permissions."},"can_manage":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may edit this connection's own settings: the owner, or `edit` access."},"can_transfer":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may transfer or delete this connection: the owner only. Narrower than `can_manage`, because an `edit` grant lets someone use and reconfigure a connection, never give it away or destroy it."},"can_revoke_share":{"type":"boolean","description":"Present on every list and detail row. Whether the caller may grant or revoke access to this connection. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `connection:share` can reconfigure or reconnect the connection but cannot change who else has access. Mirrors `DELETE /connection/{id}/share/{aclId}`."},"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. A short summary of who has access; 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the connection reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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. The new owner gets implicit `edit`, plus delete and transfer, which no grant confers; every existing grant on the connection stays as it was, so who else could already reach it is unaffected. To change who can reach a connection, use `POST /connection/{id}/share` and `DELETE /connection/{id}/share/{aclId}`. Only the Elaichi record changes: the stored credential 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. It costs the outgoing owner their access: their reach came from ownership, 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 access to the pinned connection, every entry they pinned stops resolving, for everyone including the new owner. `retain_access: true` prevents that by granting them `use` as an ordinary, listed, revocable share in the same operation; only the connection's owner may use it. Call `GET /connection/{id}/transfer-preview` first to see the same numbers. `connected_by_user_id` is not rewritten, deliberately: the stored credential still belongs to whoever authorized it, so the response carries the new `owner_user_id` beside the unchanged `connected_by_user_id` and `connected_by_label`. A client that shows \"Connected by\" from `owner_user_id` would 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":"Previews what `POST /connection/{id}/transfer` would break, before anything is written, so you can show the consequence while the decision is still open. It is a summary, not a list: exact counts plus at most five named toolboxes, breaking ones first, so it takes no `limit` or `cursor`. Gated on exactly what the transfer itself is gated on, so it never advertises something that would 403.","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 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":"Share id (`acl_…`), the `{aclId}` that `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"]},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."},"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":"Lists who a connection is shared with, cursor-paginated. This is the only place the full grant list is served; other responses carry only a short summary. Each entry carries a resolved `grantee` (name, email, and a member count for teams), so rendering a page needs no extra lookups. Filter with `grantee_type`; search member names and 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); a bare `connection:manage` permission with no grant of the caller's own does not open it.","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":{"oneOf":[{"type":"object","additionalProperties":true},{"$ref":"#/components/schemas/ShareManyResponse"}]}}}},"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 organization `view` or `use` access to this connection. Sharing never changes `owner_user_id`; that moves only through `POST /connection/{id}/transfer`. `use` 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. A private connection (no existing grants) may be shared only by its own owner; no organization permission reaches another member's private connection. 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":{"oneOf":[{"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"]}}},{"$ref":"#/components/schemas/ShareManyRequest"}]}}}},"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":"Revokes a share on a connection. The grantee loses access immediately. The connection itself, its owner, and everything already running through it are untouched. Requires `can_revoke_share` (`edit` access plus `connection:share`); a bare permission with no grant of the caller's own does not revoke a share.","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":"Share id (`acl_…`) 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. An org owner or admin sees a template only when they own it or it has been shared with them, like 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, summarized as at most a handful of connectors (the same shape 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`). `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, so 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.","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":"A short list of the grants on this template. Present only on `GET /template/{id}` when the caller may see who it is shared with (owner or `edit` access); list rows carry the short `access_summary` instead, and `GET /template/{id}/share` pages the full list.","items":{"$ref":"#/components/schemas/AclEntry"}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill."},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this template. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the template reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Lists the templates the caller owns plus every template shared with them, each with the caller's `access_level`. No organization permission widens this: an org owner or 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. An org owner or admin sees a template only when they own it or it has been shared with them, like 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, summarized as at most a handful of connectors (the same shape 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`). `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, so 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.","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":"A short list of the grants on this template. Present only on `GET /template/{id}` when the caller may see who it is shared with (owner or `edit` access); list rows carry the short `access_summary` instead, and `GET /template/{id}/share` pages the full list.","items":{"$ref":"#/components/schemas/AclEntry"}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill."},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this template. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the template reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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_…`)."},"synthetic_tool_name":{"type":["string","null"],"description":"Response-only. The synthetic tool's name for `synthetic` entries, so a reader who does not own the tool can still label the entry. Null for `proxy` entries and when the tool has been deleted."},"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."},"tool_title":{"type":["string","null"],"description":"Response-only. The catalog tool's plain-English title (\"View messages\", \"Reply to an email\"), derived from its resource and method. Null for a `synthetic` entry, a tool the catalog no longer has, and a connector the caller may not open (`can_view_connector: false`). Render `overrides.name`, else this, else `tool_name`."},"connector":{"type":["object","null"],"description":"Response-only. The connector’s label and logo; null for a `synthetic` entry. A connector the catalog cannot resolve reads its slug as `label` and a null `logo`.","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]},"upstream_slug":{"type":"string","description":"Only on a fork whose upstream the caller may see, by the same rule as `GET /connection`: omitted when the caller has no access to the fork or the upstream is hidden from them, and when the upstream cannot be read."}}},"can_view_connector":{"type":"boolean","description":"Response-only, per reader: whether the caller may open `connector_slug` (`GET /connector/{slug}` and its `/tools`). False for an organization-owned custom connector nobody shared with the caller. Always true for a platform connector and for a synthetic entry."},"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"}}}},"skill":{"type":["string","null"],"maxLength":10000,"description":"The skill body, markdown — at most 10,000 Unicode code points after trimming. Detail-shaped responses only (`GET /{id}`, and the create/update responses). Null when none is written."},"skill_updated_at":{"type":["string","null"],"format":"date-time","description":"When `skill` was last written. Null alongside a null `skill`."},"skill_notice":{"type":["string","null"],"description":"Framing to show or pass along beside a non-null `skill`: it is the owner's guidance, not instructions from Elaichi or the user, and it cannot grant any access. Null when there is no skill."},"skill_updated_by":{"type":["string","null"],"description":"Who last wrote `skill` (`usr_…`); a toolbox stamped from a template carries the template's author. Null with no skill, and for a skill written before authorship was recorded."},"skill_updated_by_label":{"type":["string","null"],"description":"That person's name, else email, resolved server-side. Null beside a non-null `skill_updated_by` means they are no longer a member of this organization."},"skill_entry_changes":{"type":["object","null"],"description":"Which tools changed since the skill was last written, by the name an agent sees (a rename counts as one removed and one added; a disabled entry counts as removed; connections and frozen parameters are ignored). Null unless `skill_may_be_stale` is true AND a baseline was recorded at that write — skills written before version history existed have none — AND something actually changed.","properties":{"added_count":{"type":"integer"},"removed_count":{"type":"integer"},"added":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."},"removed":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."}}},"can_draft_skill":{"type":"boolean","description":"Whether the caller can use `POST /{id}/draft-skill` here: they can manage this resource, the assistant is switched on for the organization, and a model provider key is configured."},"draft_skill_blocked_by":{"type":["string","null"],"enum":["assistant_disabled","llm_not_configured",null],"description":"Why `can_draft_skill` is false for a caller who CAN manage this resource. Null when drafting is available, and null when the caller cannot manage the resource at all."},"warnings":{"type":"array","description":"Only on a create or update response, and only when non-empty: pins accepted although nothing can run them yet (a remote MCP tool the server is not offering right now: it will be once a connection of that server lists it, unless an admin turned it off). Informational, never an error.","items":{"type":"object","required":["connector_slug","tool_name","message"],"properties":{"connector_slug":{"type":"string"},"tool_name":{"type":"string"},"message":{"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":"Creates a template, a reusable set of tool entries. 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 there is no connection warning; the one thing it can warn about is a pin on a remote MCP tool the server is not offering right now (no connection lists it yet), which is accepted and answered with a non-empty `warnings` array (`{ connector_slug, tool_name, message }`) on the 201. `warnings` is absent when there is nothing to say.","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"},"id":{"type":["string","null"],"maxLength":200,"description":"Optional identity hint (`tbxe_…` on a toolbox, `tple_…` on a template): the `id` of a row this resource already has. On `PATCH /toolbox/{id}` and `PATCH /template/{id}` a row echoed back with its own id keeps it, so a client that keys per-row state by entry id (a switch mid-save) is not orphaned by the replacement. It is a hint, never an assertion: an id that is not one of THIS resource's current rows, is repeated, or would put the row out of stored order (rows list in id order) is ignored and the row gets a fresh id — and so is a value that is not a usable string (not a string, or over 200 characters). Never a 400, and never adopted from another resource. Omit it for a new row. Ignored on create."}}}},"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"}},"skill":{"type":["string","null"],"maxLength":10000,"description":"Markdown guidance on how to use these tools well. Trimmed, then capped at 10,000 Unicode code points (an emoji counts once) — longer is a `400` naming the limit, never a silent cut. `null` or an empty string clears it; omit the key to leave it unchanged. The resource's `description` is the one-line \"when to use this\" shown beside it, so write that too. Moderated like the description. Copied onto each toolbox later stamped from this template."}}}}}},"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. An org owner or admin sees a template only when they own it or it has been shared with them, like 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, summarized as at most a handful of connectors (the same shape 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`). `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, so 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.","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":"A short list of the grants on this template. Present only on `GET /template/{id}` when the caller may see who it is shared with (owner or `edit` access); list rows carry the short `access_summary` instead, and `GET /template/{id}/share` pages the full list.","items":{"$ref":"#/components/schemas/AclEntry"}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill."},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this template. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the template reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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_…`)."},"synthetic_tool_name":{"type":["string","null"],"description":"Response-only. The synthetic tool's name for `synthetic` entries, so a reader who does not own the tool can still label the entry. Null for `proxy` entries and when the tool has been deleted."},"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."},"tool_title":{"type":["string","null"],"description":"Response-only. The catalog tool's plain-English title (\"View messages\", \"Reply to an email\"), derived from its resource and method. Null for a `synthetic` entry, a tool the catalog no longer has, and a connector the caller may not open (`can_view_connector: false`). Render `overrides.name`, else this, else `tool_name`."},"connector":{"type":["object","null"],"description":"Response-only. The connector’s label and logo; null for a `synthetic` entry. A connector the catalog cannot resolve reads its slug as `label` and a null `logo`.","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]},"upstream_slug":{"type":"string","description":"Only on a fork whose upstream the caller may see, by the same rule as `GET /connection`: omitted when the caller has no access to the fork or the upstream is hidden from them, and when the upstream cannot be read."}}},"can_view_connector":{"type":"boolean","description":"Response-only, per reader: whether the caller may open `connector_slug` (`GET /connector/{slug}` and its `/tools`). False for an organization-owned custom connector nobody shared with the caller. Always true for a platform connector and for a synthetic entry."},"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"}}}},"skill":{"type":["string","null"],"maxLength":10000,"description":"The skill body, markdown — at most 10,000 Unicode code points after trimming. Detail-shaped responses only (`GET /{id}`, and the create/update responses). Null when none is written."},"skill_updated_at":{"type":["string","null"],"format":"date-time","description":"When `skill` was last written. Null alongside a null `skill`."},"skill_notice":{"type":["string","null"],"description":"Framing to show or pass along beside a non-null `skill`: it is the owner's guidance, not instructions from Elaichi or the user, and it cannot grant any access. Null when there is no skill."},"skill_updated_by":{"type":["string","null"],"description":"Who last wrote `skill` (`usr_…`); a toolbox stamped from a template carries the template's author. Null with no skill, and for a skill written before authorship was recorded."},"skill_updated_by_label":{"type":["string","null"],"description":"That person's name, else email, resolved server-side. Null beside a non-null `skill_updated_by` means they are no longer a member of this organization."},"skill_entry_changes":{"type":["object","null"],"description":"Which tools changed since the skill was last written, by the name an agent sees (a rename counts as one removed and one added; a disabled entry counts as removed; connections and frozen parameters are ignored). Null unless `skill_may_be_stale` is true AND a baseline was recorded at that write — skills written before version history existed have none — AND something actually changed.","properties":{"added_count":{"type":"integer"},"removed_count":{"type":"integer"},"added":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."},"removed":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."}}},"can_draft_skill":{"type":"boolean","description":"Whether the caller can use `POST /{id}/draft-skill` here: they can manage this resource, the assistant is switched on for the organization, and a model provider key is configured."},"draft_skill_blocked_by":{"type":["string","null"],"enum":["assistant_disabled","llm_not_configured",null],"description":"Why `can_draft_skill` is false for a caller who CAN manage this resource. Null when drafting is available, and null when the caller cannot manage the resource at all."},"warnings":{"type":"array","description":"Only on a create or update response, and only when non-empty: pins accepted although nothing can run them yet (a remote MCP tool the server is not offering right now: it will be once a connection of that server lists it, unless an admin turned it off). Informational, never an error.","items":{"type":"object","required":["connector_slug","tool_name","message"],"properties":{"connector_slug":{"type":"string"},"tool_name":{"type":"string"},"message":{"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":"Returns one template with its full entry list (a template never carries a `connection_id`), and `access_summary` when the caller may see who it is shared with. Only the owner or someone with a grant can open a template; anyone else gets `404`, never `403`, including a holder of `template:manage`.","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. An org owner or admin sees a template only when they own it or it has been shared with them, like 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, summarized as at most a handful of connectors (the same shape 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`). `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, so 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.","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":"A short list of the grants on this template. Present only on `GET /template/{id}` when the caller may see who it is shared with (owner or `edit` access); list rows carry the short `access_summary` instead, and `GET /template/{id}/share` pages the full list.","items":{"$ref":"#/components/schemas/AclEntry"}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill."},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this template. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the template reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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_…`)."},"synthetic_tool_name":{"type":["string","null"],"description":"Response-only. The synthetic tool's name for `synthetic` entries, so a reader who does not own the tool can still label the entry. Null for `proxy` entries and when the tool has been deleted."},"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."},"tool_title":{"type":["string","null"],"description":"Response-only. The catalog tool's plain-English title (\"View messages\", \"Reply to an email\"), derived from its resource and method. Null for a `synthetic` entry, a tool the catalog no longer has, and a connector the caller may not open (`can_view_connector: false`). Render `overrides.name`, else this, else `tool_name`."},"connector":{"type":["object","null"],"description":"Response-only. The connector’s label and logo; null for a `synthetic` entry. A connector the catalog cannot resolve reads its slug as `label` and a null `logo`.","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]},"upstream_slug":{"type":"string","description":"Only on a fork whose upstream the caller may see, by the same rule as `GET /connection`: omitted when the caller has no access to the fork or the upstream is hidden from them, and when the upstream cannot be read."}}},"can_view_connector":{"type":"boolean","description":"Response-only, per reader: whether the caller may open `connector_slug` (`GET /connector/{slug}` and its `/tools`). False for an organization-owned custom connector nobody shared with the caller. Always true for a platform connector and for a synthetic entry."},"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"}}}},"skill":{"type":["string","null"],"maxLength":10000,"description":"The skill body, markdown — at most 10,000 Unicode code points after trimming. Detail-shaped responses only (`GET /{id}`, and the create/update responses). Null when none is written."},"skill_updated_at":{"type":["string","null"],"format":"date-time","description":"When `skill` was last written. Null alongside a null `skill`."},"skill_notice":{"type":["string","null"],"description":"Framing to show or pass along beside a non-null `skill`: it is the owner's guidance, not instructions from Elaichi or the user, and it cannot grant any access. Null when there is no skill."},"skill_updated_by":{"type":["string","null"],"description":"Who last wrote `skill` (`usr_…`); a toolbox stamped from a template carries the template's author. Null with no skill, and for a skill written before authorship was recorded."},"skill_updated_by_label":{"type":["string","null"],"description":"That person's name, else email, resolved server-side. Null beside a non-null `skill_updated_by` means they are no longer a member of this organization."},"skill_entry_changes":{"type":["object","null"],"description":"Which tools changed since the skill was last written, by the name an agent sees (a rename counts as one removed and one added; a disabled entry counts as removed; connections and frozen parameters are ignored). Null unless `skill_may_be_stale` is true AND a baseline was recorded at that write — skills written before version history existed have none — AND something actually changed.","properties":{"added_count":{"type":"integer"},"removed_count":{"type":"integer"},"added":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."},"removed":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."}}},"can_draft_skill":{"type":"boolean","description":"Whether the caller can use `POST /{id}/draft-skill` here: they can manage this resource, the assistant is switched on for the organization, and a model provider key is configured."},"draft_skill_blocked_by":{"type":["string","null"],"enum":["assistant_disabled","llm_not_configured",null],"description":"Why `can_draft_skill` is false for a caller who CAN manage this resource. Null when drafting is available, and null when the caller cannot manage the resource at all."},"warnings":{"type":"array","description":"Only on a create or update response, and only when non-empty: pins accepted although nothing can run them yet (a remote MCP tool the server is not offering right now: it will be once a connection of that server lists it, unless an admin turned it off). Informational, never an error.","items":{"type":"object","required":["connector_slug","tool_name","message"],"properties":{"connector_slug":{"type":"string"},"tool_name":{"type":"string"},"message":{"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":"Updates a template. **`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 and the skill once). `skill` is an ordinary field too: a write clears `skill_may_be_stale`, and an `entries` write sets it when a skill exists and the same request did not also write `skill`. Every skill edit is audited on `template.updated` (`metadata.skill`: `written`/`reaffirmed`/`cleared`, plus `metadata.skill_version_id`, never the text) and kept in `GET /template/{id}/skill-version`. Requires `edit` access or ownership (own grant) — no `template:manage` fallback; a `view`/`use` grantee reads the skill and gets `403` writing it. An `entries` write answers the same non-empty `warnings` array as create for a pin on a remote MCP tool the server is not offering right now (no connection lists it yet).","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"},"id":{"type":["string","null"],"maxLength":200,"description":"Optional identity hint (`tbxe_…` on a toolbox, `tple_…` on a template): the `id` of a row this resource already has. On `PATCH /toolbox/{id}` and `PATCH /template/{id}` a row echoed back with its own id keeps it, so a client that keys per-row state by entry id (a switch mid-save) is not orphaned by the replacement. It is a hint, never an assertion: an id that is not one of THIS resource's current rows, is repeated, or would put the row out of stored order (rows list in id order) is ignored and the row gets a fresh id — and so is a value that is not a usable string (not a string, or over 200 characters). Never a 400, and never adopted from another resource. Omit it for a new row. Ignored on create."}}},"description":"Replaces every entry."},"skill":{"type":["string","null"],"maxLength":10000,"description":"Markdown guidance on how to use these tools well. Trimmed, then capped at 10,000 Unicode code points (an emoji counts once) — longer is a `400` naming the limit, never a silent cut. `null` or an empty string clears it; omit the key to leave it unchanged. The resource's `description` is the one-line \"when to use this\" shown beside it, so write that too. Moderated like the description."}}}}}},"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":"Deletes a template. 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}/skill-version":{"get":{"operationId":"listTemplateSkillVersions","summary":"List a template's skill versions","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":"Version id (`skv_…`)."},"kind":{"type":"string","enum":["written","reaffirmed","cleared","copied"],"description":"`written`: new text. `reaffirmed`: the same text saved again (\"still accurate\"). `cleared`: the skill was removed. `copied`: a toolbox stamped from a template started with its skill."},"excerpt":{"type":["string","null"],"description":"The opening of the text as one plain line, at most 200 code points. Null for `cleared`."},"char_count":{"type":"integer","description":"Length in Unicode code points; 0 for `cleared`."},"author":{"type":["string","null"],"description":"Who made this write (`usr_…`)."},"author_label":{"type":["string","null"],"description":"Their name, else email. Null beside a non-null `author`: no longer a member."},"origin_template_id":{"type":["string","null"],"description":"`copied` rows: the template it came from."},"origin_template_name":{"type":["string","null"],"description":"Its current name; null once deleted."},"created_at":{"type":"string","format":"date-time"},"is_current":{"type":"boolean","description":"True on the newest version — the text the resource holds now."},"matches_current":{"type":"boolean","description":"True when this version's text equals the resource's current skill (restoring it would change nothing). Not the same as `is_current`: a later \"still accurate\" save repeats an older version's text, and both match."}}}},"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 write of the template's `skill`, newest first, cursor-paginated — the 50 newest are kept per template, older ones are pruned on the next write. Restoring a version is an ordinary `PATCH /template/{id} { skill }` with its text, which records a new version. Requires `edit` access or ownership: `404` when the caller cannot see the template, `403` when they can see it below `edit`.","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"}]}},"/template/{id}/skill-version/{versionId}":{"get":{"operationId":"getTemplateSkillVersion","summary":"Get one template skill version with its text","tags":["Templates"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Version id (`skv_…`)."},"kind":{"type":"string","enum":["written","reaffirmed","cleared","copied"],"description":"`written`: new text. `reaffirmed`: the same text saved again (\"still accurate\"). `cleared`: the skill was removed. `copied`: a toolbox stamped from a template started with its skill."},"excerpt":{"type":["string","null"],"description":"The opening of the text as one plain line, at most 200 code points. Null for `cleared`."},"char_count":{"type":"integer","description":"Length in Unicode code points; 0 for `cleared`."},"author":{"type":["string","null"],"description":"Who made this write (`usr_…`)."},"author_label":{"type":["string","null"],"description":"Their name, else email. Null beside a non-null `author`: no longer a member."},"origin_template_id":{"type":["string","null"],"description":"`copied` rows: the template it came from."},"origin_template_name":{"type":["string","null"],"description":"Its current name; null once deleted."},"created_at":{"type":"string","format":"date-time"},"is_current":{"type":"boolean","description":"True on the newest version — the text the resource holds now."},"matches_current":{"type":"boolean","description":"True when this version's text equals the resource's current skill (restoring it would change nothing). Not the same as `is_current`: a later \"still accurate\" save repeats an older version's text, and both match."}}},{"type":"object","properties":{"skill":{"type":["string","null"],"description":"The full text of this version; null for `cleared`."}}}]}}}},"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":"Same access rule as the list. A version id that belongs to a different template is a `404`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Template id (`tpl_…`)."},{"name":"versionId","in":"path","required":true,"schema":{"type":"string"},"description":"Version id (`skv_…`)."}]}},"/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. An org owner or admin sees a template only when they own it or it has been shared with them, like 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer"},"connectors":{"type":"object","description":"Which integrations this template's tools come from, summarized as at most a handful of connectors (the same shape 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`). `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, so 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.","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":"A short list of the grants on this template. Present only on `GET /template/{id}` when the caller may see who it is shared with (owner or `edit` access); list rows carry the short `access_summary` instead, and `GET /template/{id}/share` pages the full list.","items":{"$ref":"#/components/schemas/AclEntry"}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill."},"can_use":{"type":"boolean","description":"Whether the caller may stamp a toolbox from this template — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this template. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `template:share` can edit the template but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the template reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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 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":"Share id (`acl_…`), the `{aclId}` that `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"]},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."},"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":"Lists who a template is shared with. 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":{"oneOf":[{"$ref":"#/components/schemas/AclEntry"},{"$ref":"#/components/schemas/ShareManyResponse"}]}}}},"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":{"oneOf":[{"$ref":"#/components/schemas/ShareRequest"},{"$ref":"#/components/schemas/ShareManyRequest"}]}}}},"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":"Revokes a share on a template. 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":"Share id (`acl_…`) 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, shareable 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."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Number of proxy entries with no connection pinned (`connection_id` is null), which is what a \"N need connection\" status is built from. Excludes synthetic entries, which pin no `connection_id` of their own by design. Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in the detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, summarized as at most a handful of connectors. Present on every `GET /toolbox` row: a stored row summarizes its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarizes the connections it spans. Also present on every command response (`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-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, so 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.","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` or `use` grantee, because who else has access is information about colleagues. Read `can_see_shares` to tell \"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 short preview of who the toolbox is shared with (same cap as `access_summary`). Present only on `GET /toolbox/{id}` when the caller may see who it is shared with. Never on a list row (`GET /toolbox`); page `GET /toolbox/{id}/share` for the full, cursor-paginated list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a short summary. Present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see who it is shared with (the same condition as `access_summary`). 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."}}}}}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response. Omitted on a dynamic row, which has no skill of its own."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill. Omitted on a dynamic row."},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this toolbox. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the toolbox reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Lists the toolboxes the caller owns plus every toolbox shared with them, each with the caller's `access_level`, plus two kinds of automatic toolbox (`global:{user_id}`, and one `connection:{connection_id}` per active connection the caller can use) appended to the last page only (the one whose `next_cursor` is null), always with `access_level: \"owner\"`. So the order across the whole listing is stored toolboxes first, automatic ones after. No organization permission, not even `toolbox:view` or `toolbox:manage`, widens this beyond what the caller owns or was granted, and a private toolbox never appears for anyone but its owner and its grantees. `?q=` matches the name as a substring. `?type=stored` drops the automatic rows, which suits a toolbox picker. `?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 real, shareable toolboxes, omitting the automatic `global:…`/`connection:…` rows; `automatic` is the mirror, paging the automatic 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, or stamp one from a template","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, shareable 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."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Number of proxy entries with no connection pinned (`connection_id` is null), which is what a \"N need connection\" status is built from. Excludes synthetic entries, which pin no `connection_id` of their own by design. Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in the detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, summarized as at most a handful of connectors. Present on every `GET /toolbox` row: a stored row summarizes its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarizes the connections it spans. Also present on every command response (`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-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, so 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.","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` or `use` grantee, because who else has access is information about colleagues. Read `can_see_shares` to tell \"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 short preview of who the toolbox is shared with (same cap as `access_summary`). Present only on `GET /toolbox/{id}` when the caller may see who it is shared with. Never on a list row (`GET /toolbox`); page `GET /toolbox/{id}/share` for the full, cursor-paginated list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a short summary. Present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see who it is shared with (the same condition as `access_summary`). 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."}}}}}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response. Omitted on a dynamic row, which has no skill of its own."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill. Omitted on a dynamic row."},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this toolbox. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the toolbox reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Set by the server, never a client input: whose `use` grant on `connection_id` this pin rides on. If this user loses `use` on the connection, the entry goes unmet for every executor, not just them. Null if and only if `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`."},"unmet_reason":{"type":["string","null"],"enum":["connector_not_shared",null],"description":"Detail responses only: the one `unmet` cause worth naming, null for every other. `connector_not_shared`: the pinned connection is live and delegation-intact, but its OWNER no longer holds `use` on the org-owned connector it runs through, so the entry is listed (flagged) and never advertised, and every call through it is refused. Re-sharing the connector clears it with no edit to the toolbox. Not repairable by re-pinning — `can_repin` stays false for it."},"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`, using the same precedence as the caller's own `access_via` (owner, else org, else team, else direct; the broadest wins). `delegation_ok` says the delegation still stands; this says what it stands on, so a client 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 recorded delegator, and whenever the delegation no longer works (`delegation_ok: false`): the delegator is no longer an active member, or their access has fallen below `use`."},"can_repin":{"type":"boolean","description":"Detail responses only, and the only field here that differs by reader: whether the caller may 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."},"can_view_connector":{"type":"boolean","description":"Present on the toolbox's own responses, per reader: whether the caller may open `connector_slug` (`GET /connector/{slug}` and its `/tools`). False when the entry is pinned to an organization-owned custom connector that nobody has shared with the caller; those routes answer 404 for them. The entry still carries its label and logo and still runs on its delegator's access; this only says the connector page and its tool picker are not for this reader. Always true for a platform connector and for a synthetic entry."},"tool_title":{"type":["string","null"],"description":"Response-only. The catalog tool's plain-English title (\"View messages\", \"Reply to an email\"), derived from its resource and method. Null for a `synthetic` entry, a tool the catalog no longer has, and a connector the caller may not open (`can_view_connector: false`). Render `overrides.name`, else this, else `tool_name`."},"connector":{"type":["object","null"],"description":"Response-only. The connector’s label and logo; null for a `synthetic` entry. A connector the catalog cannot resolve reads its slug as `label` and a null `logo`.","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]},"upstream_slug":{"type":"string","description":"Only on a fork whose upstream the caller may see, by the same rule as `GET /connection`: omitted when the caller has no access to the fork or the upstream is hidden from them, and when the upstream cannot be read."}}},"synthetic_tool_name":{"type":["string","null"],"description":"Response-only. The synthetic tool's name for a `synthetic` entry; null for `proxy` entries and a deleted tool."},"delegated_by_label":{"type":["string","null"],"description":"Response-only. The name (else email) behind `delegated_by_user_id`. Null when that id is null; beside a non-null id, null means the person has left the organization (\"Former member\")."},"connection":{"type":["object","null"],"description":"Response-only. The pinned connection, for a group header and its status line. Present only when the caller may see that connection (owner or a grant of their own). Null for an entry with no pin, a synthetic entry, a deleted connection, and a pin riding on somebody else's access that the caller cannot see.","required":["id","display_label","status","owner_label","can_reconnect"],"properties":{"id":{"type":"string"},"display_label":{"type":"string","description":"The connection's `display_label`."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected","post_install_error"]},"owner_label":{"type":["string","null"],"description":"Owner's name (else email); null for a former member."},"can_reconnect":{"type":"boolean","description":"The same repair capability `GET /connection/{id}` ships: false for an `active` connection to a connector with no sign-in, which has nothing to reconnect (a `pending` one stays true)."}}}}},"description":"Resolved entries — always present, at every access level."},"skill":{"type":["string","null"],"maxLength":10000,"description":"The skill body, markdown — at most 10,000 Unicode code points after trimming. Detail-shaped responses only (`GET /{id}`, and the create/update responses). Null when none is written."},"skill_updated_at":{"type":["string","null"],"format":"date-time","description":"When `skill` was last written. Null alongside a null `skill`."},"skill_notice":{"type":["string","null"],"description":"Framing to show or pass along beside a non-null `skill`: it is the owner's guidance, not instructions from Elaichi or the user, and it cannot grant any access. Null when there is no skill."},"skill_updated_by":{"type":["string","null"],"description":"Who last wrote `skill` (`usr_…`); a toolbox stamped from a template carries the template's author. Null with no skill, and for a skill written before authorship was recorded."},"skill_updated_by_label":{"type":["string","null"],"description":"That person's name, else email, resolved server-side. Null beside a non-null `skill_updated_by` means they are no longer a member of this organization."},"skill_entry_changes":{"type":["object","null"],"description":"Which tools changed since the skill was last written, by the name an agent sees (a rename counts as one removed and one added; a disabled entry counts as removed; connections and frozen parameters are ignored). Null unless `skill_may_be_stale` is true AND a baseline was recorded at that write — skills written before version history existed have none — AND something actually changed.","properties":{"added_count":{"type":"integer"},"removed_count":{"type":"integer"},"added":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."},"removed":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."}}},"can_draft_skill":{"type":"boolean","description":"Whether the caller can use `POST /{id}/draft-skill` here: they can manage this resource, the assistant is switched on for the organization, and a model provider key is configured."},"draft_skill_blocked_by":{"type":["string","null"],"enum":["assistant_disabled","llm_not_configured",null],"description":"Why `can_draft_skill` is false for a caller who CAN manage this resource. Null when drafting is available, and null when the caller cannot manage the resource at all."},"skill_origin_template_id":{"type":["string","null"],"description":"The template this toolbox's CURRENT skill was copied from verbatim at stamping. Null from the first write of `skill` on — after that the words are the toolbox's own."},"skill_origin_template_name":{"type":["string","null"],"description":"That template's current name, resolved server-side; null with no origin, or once the template is gone."},"template_name":{"type":["string","null"],"description":"`GET /toolbox/{id}` only. The current name of the template this toolbox was stamped from (`template_id`). Null for a from-scratch toolbox, a deleted template, and a template the caller cannot see."},"skill_tool_issues":{"type":"object","description":"Stored toolboxes only. Tool-shaped names the skill puts in backticks that no entry of this toolbox holds (what an editor shows as \"tools named here aren't in this toolbox\"). A name matches an entry's catalog `tool_name`, or its `overrides.name` when renamed. A count plus at most 10 names; `count` is 0 when there is no skill.","properties":{"count":{"type":"integer","description":"Distinct names the skill uses that no entry holds."},"preview":{"type":"array","maxItems":10,"items":{"type":"object","properties":{"name":{"type":"string","description":"The name as the skill writes it."},"suggestion":{"type":["string","null"],"description":"The entry's own name this most likely means (`list_query_database` -> `list_all_notion_query_database`); null unless exactly one entry matches."}}}}}},"skill_agent_preview":{"type":["object","null"],"description":"Stored toolboxes only: what an agent connected to this toolbox is shown before it reads the skill, built by the same code the MCP endpoint uses. `instructions_line` is the line the server instructions carry for a connection limited to chosen toolboxes; `resource` is the `elaichi://toolbox/{id}/skill` entry in `resources/list`. The resource text itself is `skill_notice`, a blank line, then `skill`. Null with no skill.","properties":{"instructions_line":{"type":"string"},"resource":{"type":"object","properties":{"uri":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"mime_type":{"type":"string"}}}}},"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, labeled 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. 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. This is a summary, never the full set, so the response stays the same size however many connections the toolbox spans. 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 or share response it can be `null`: the write succeeded but the summary could not be computed (this is not the same as \"nothing is delegated\", which is an all-zero summary); re-read `GET /toolbox/{id}`. 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. Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection that `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"},"logo":{"type":["string","null"],"description":"The connector's logo."},"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 recorded delegator (an older entry), so `delegated_by` is the connection's owner standing in as the delegator. 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, with the same precedence as `access_via`: 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, a client reads an org-wide or team grant as a personal favor 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 access to the connection has fallen below `use`), since the only honest statement left is who pinned it."},"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. Absent otherwise, and a client should then say \"a team\" without naming it, because the team's name is not the caller's to learn.","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":"Creates a toolbox. Requires `toolbox:create`. There are two ways to build one, and they combine. Pass `template_id` to stamp it: the template's entries are copied once, at creation time (never a live link), and you need `use` access to the template. With `connection_map` (connector_slug -> connection_id), each stamped proxy entry's connector is filled from one of the caller's own usable connections, and 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 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 (the grantee runs tools on the sharer's own connections), which is what the response's `delegation` summary discloses. It is always present, not only when `shares` was sent, so it can be read before the 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"},"id":{"type":["string","null"],"maxLength":200,"description":"Optional identity hint (`tbxe_…` on a toolbox, `tple_…` on a template): the `id` of a row this resource already has. On `PATCH /toolbox/{id}` and `PATCH /template/{id}` a row echoed back with its own id keeps it, so a client that keys per-row state by entry id (a switch mid-save) is not orphaned by the replacement. It is a hint, never an assertion: an id that is not one of THIS resource's current rows, is repeated, or would put the row out of stored order (rows list in id order) is ignored and the row gets a fresh id — and so is a value that is not a usable string (not a string, or over 200 characters). Never a 400, and never adopted from another resource. Omit it for a new row. Ignored on create."},"connection_id":{"type":["string","null"],"description":"Pin this entry to one connection (`conn_…`) the editor making the request can use; they become its delegator. `null` = \"needs connection.\" On `PATCH /toolbox/{id}`, an incoming entry whose (connection_id, tool_name) pair matches an existing row keeps that row's original delegator rather than re-stamping the caller; 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"}},"skill":{"type":["string","null"],"maxLength":10000,"description":"Markdown guidance on how to use these tools well. Trimmed, then capped at 10,000 Unicode code points (an emoji counts once) — longer is a `400` naming the limit, never a silent cut. `null` or an empty string clears it; omit the key to leave it unchanged. The resource's `description` is the one-line \"when to use this\" shown beside it, so write that too. Moderated like the description. When stamping, omitting it copies the template's skill (and `skill_origin_template_id` names the template); sending it — a string, or null for none — wins."}}}}}},"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":"Lists the connectors that appear in the toolboxes `GET /toolbox` would return for this caller, and how many of those toolboxes hold each one. These are 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. Filtering on any slug returned here yields exactly that row's `toolbox_count` toolboxes from `GET /toolbox` under the same `?type=`, so pass `?type=` here whenever you pass it there. `toolbox_count` counts distinct toolboxes over the caller's whole visible set, never over one page of it. Ordered most-used first, slug ascending as the tiebreak. Cursor-paginated. Visibility follows `GET /toolbox`: a connector reachable only through someone else's private toolbox is not listed.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Prefix match on the connector slug (`goo` matches `google-drive`), not on the display name. 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 real, shareable toolboxes, `automatic` only the automatic `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 with its resolved tools","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, shareable 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."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Number of proxy entries with no connection pinned (`connection_id` is null), which is what a \"N need connection\" status is built from. Excludes synthetic entries, which pin no `connection_id` of their own by design. Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in the detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, summarized as at most a handful of connectors. Present on every `GET /toolbox` row: a stored row summarizes its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarizes the connections it spans. Also present on every command response (`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-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, so 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.","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` or `use` grantee, because who else has access is information about colleagues. Read `can_see_shares` to tell \"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 short preview of who the toolbox is shared with (same cap as `access_summary`). Present only on `GET /toolbox/{id}` when the caller may see who it is shared with. Never on a list row (`GET /toolbox`); page `GET /toolbox/{id}/share` for the full, cursor-paginated list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a short summary. Present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see who it is shared with (the same condition as `access_summary`). 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."}}}}}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response. Omitted on a dynamic row, which has no skill of its own."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill. Omitted on a dynamic row."},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this toolbox. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the toolbox reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Set by the server, never a client input: whose `use` grant on `connection_id` this pin rides on. If this user loses `use` on the connection, the entry goes unmet for every executor, not just them. Null if and only if `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`."},"unmet_reason":{"type":["string","null"],"enum":["connector_not_shared",null],"description":"Detail responses only: the one `unmet` cause worth naming, null for every other. `connector_not_shared`: the pinned connection is live and delegation-intact, but its OWNER no longer holds `use` on the org-owned connector it runs through, so the entry is listed (flagged) and never advertised, and every call through it is refused. Re-sharing the connector clears it with no edit to the toolbox. Not repairable by re-pinning — `can_repin` stays false for it."},"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`, using the same precedence as the caller's own `access_via` (owner, else org, else team, else direct; the broadest wins). `delegation_ok` says the delegation still stands; this says what it stands on, so a client 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 recorded delegator, and whenever the delegation no longer works (`delegation_ok: false`): the delegator is no longer an active member, or their access has fallen below `use`."},"can_repin":{"type":"boolean","description":"Detail responses only, and the only field here that differs by reader: whether the caller may 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."},"can_view_connector":{"type":"boolean","description":"Present on the toolbox's own responses, per reader: whether the caller may open `connector_slug` (`GET /connector/{slug}` and its `/tools`). False when the entry is pinned to an organization-owned custom connector that nobody has shared with the caller; those routes answer 404 for them. The entry still carries its label and logo and still runs on its delegator's access; this only says the connector page and its tool picker are not for this reader. Always true for a platform connector and for a synthetic entry."},"tool_title":{"type":["string","null"],"description":"Response-only. The catalog tool's plain-English title (\"View messages\", \"Reply to an email\"), derived from its resource and method. Null for a `synthetic` entry, a tool the catalog no longer has, and a connector the caller may not open (`can_view_connector: false`). Render `overrides.name`, else this, else `tool_name`."},"connector":{"type":["object","null"],"description":"Response-only. The connector’s label and logo; null for a `synthetic` entry. A connector the catalog cannot resolve reads its slug as `label` and a null `logo`.","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]},"upstream_slug":{"type":"string","description":"Only on a fork whose upstream the caller may see, by the same rule as `GET /connection`: omitted when the caller has no access to the fork or the upstream is hidden from them, and when the upstream cannot be read."}}},"synthetic_tool_name":{"type":["string","null"],"description":"Response-only. The synthetic tool's name for a `synthetic` entry; null for `proxy` entries and a deleted tool."},"delegated_by_label":{"type":["string","null"],"description":"Response-only. The name (else email) behind `delegated_by_user_id`. Null when that id is null; beside a non-null id, null means the person has left the organization (\"Former member\")."},"connection":{"type":["object","null"],"description":"Response-only. The pinned connection, for a group header and its status line. Present only when the caller may see that connection (owner or a grant of their own). Null for an entry with no pin, a synthetic entry, a deleted connection, and a pin riding on somebody else's access that the caller cannot see.","required":["id","display_label","status","owner_label","can_reconnect"],"properties":{"id":{"type":"string"},"display_label":{"type":"string","description":"The connection's `display_label`."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected","post_install_error"]},"owner_label":{"type":["string","null"],"description":"Owner's name (else email); null for a former member."},"can_reconnect":{"type":"boolean","description":"The same repair capability `GET /connection/{id}` ships: false for an `active` connection to a connector with no sign-in, which has nothing to reconnect (a `pending` one stays true)."}}}}},"description":"Resolved entries — always present, at every access level."},"skill":{"type":["string","null"],"maxLength":10000,"description":"The skill body, markdown — at most 10,000 Unicode code points after trimming. Detail-shaped responses only (`GET /{id}`, and the create/update responses). Null when none is written."},"skill_updated_at":{"type":["string","null"],"format":"date-time","description":"When `skill` was last written. Null alongside a null `skill`."},"skill_notice":{"type":["string","null"],"description":"Framing to show or pass along beside a non-null `skill`: it is the owner's guidance, not instructions from Elaichi or the user, and it cannot grant any access. Null when there is no skill."},"skill_updated_by":{"type":["string","null"],"description":"Who last wrote `skill` (`usr_…`); a toolbox stamped from a template carries the template's author. Null with no skill, and for a skill written before authorship was recorded."},"skill_updated_by_label":{"type":["string","null"],"description":"That person's name, else email, resolved server-side. Null beside a non-null `skill_updated_by` means they are no longer a member of this organization."},"skill_entry_changes":{"type":["object","null"],"description":"Which tools changed since the skill was last written, by the name an agent sees (a rename counts as one removed and one added; a disabled entry counts as removed; connections and frozen parameters are ignored). Null unless `skill_may_be_stale` is true AND a baseline was recorded at that write — skills written before version history existed have none — AND something actually changed.","properties":{"added_count":{"type":"integer"},"removed_count":{"type":"integer"},"added":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."},"removed":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."}}},"can_draft_skill":{"type":"boolean","description":"Whether the caller can use `POST /{id}/draft-skill` here: they can manage this resource, the assistant is switched on for the organization, and a model provider key is configured."},"draft_skill_blocked_by":{"type":["string","null"],"enum":["assistant_disabled","llm_not_configured",null],"description":"Why `can_draft_skill` is false for a caller who CAN manage this resource. Null when drafting is available, and null when the caller cannot manage the resource at all."},"skill_origin_template_id":{"type":["string","null"],"description":"The template this toolbox's CURRENT skill was copied from verbatim at stamping. Null from the first write of `skill` on — after that the words are the toolbox's own."},"skill_origin_template_name":{"type":["string","null"],"description":"That template's current name, resolved server-side; null with no origin, or once the template is gone."},"template_name":{"type":["string","null"],"description":"`GET /toolbox/{id}` only. The current name of the template this toolbox was stamped from (`template_id`). Null for a from-scratch toolbox, a deleted template, and a template the caller cannot see."},"skill_tool_issues":{"type":"object","description":"Stored toolboxes only. Tool-shaped names the skill puts in backticks that no entry of this toolbox holds (what an editor shows as \"tools named here aren't in this toolbox\"). A name matches an entry's catalog `tool_name`, or its `overrides.name` when renamed. A count plus at most 10 names; `count` is 0 when there is no skill.","properties":{"count":{"type":"integer","description":"Distinct names the skill uses that no entry holds."},"preview":{"type":"array","maxItems":10,"items":{"type":"object","properties":{"name":{"type":"string","description":"The name as the skill writes it."},"suggestion":{"type":["string","null"],"description":"The entry's own name this most likely means (`list_query_database` -> `list_all_notion_query_database`); null unless exactly one entry matches."}}}}}},"skill_agent_preview":{"type":["object","null"],"description":"Stored toolboxes only: what an agent connected to this toolbox is shown before it reads the skill, built by the same code the MCP endpoint uses. `instructions_line` is the line the server instructions carry for a connection limited to chosen toolboxes; `resource` is the `elaichi://toolbox/{id}/skill` entry in `resources/list`. The resource text itself is `skill_notice`, a blank line, then `skill`. Null with no skill.","properties":{"instructions_line":{"type":"string"},"resource":{"type":"object","properties":{"uri":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"mime_type":{"type":"string"}}}}},"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, labeled 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. 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. This is a summary, never the full set, so the response stays the same size however many connections the toolbox spans. 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 or share response it can be `null`: the write succeeded but the summary could not be computed (this is not the same as \"nothing is delegated\", which is an all-zero summary); re-read `GET /toolbox/{id}`. 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. Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection that `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"},"logo":{"type":["string","null"],"description":"The connector's logo."},"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 recorded delegator (an older entry), so `delegated_by` is the connection's owner standing in as the delegator. 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, with the same precedence as `access_via`: 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, a client reads an org-wide or team grant as a personal favor 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 access to the connection has fallen below `use`), since the only honest statement left is who pinned it."},"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. Absent otherwise, and a client should then say \"a team\" without naming it, because the team's name is not the caller's to learn.","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":"Returns one toolbox with its resolved entry list (always, even at `view` access, with `unmet`, `tool_available`, `delegation_ok` and `connection_status` computed live) and, only when the caller's access reaches `use`, the resolved `tools[]` that a client reaching this toolbox through MCP would see (a view-only grantee must not see delegated connections' labels). `access_summary` and `connected_app_summary` are present when the caller may see who the toolbox is shared with; the second is a short summary of the OAuth-connected apps that reach this toolbox, paged in full by `GET /toolbox/{id}/connected-app`. Accepts an automatic id (`global:…` or `connection:…`), which is always fully the caller's own to use. A toolbox the caller neither owns nor was granted is `404`, for everyone, including a holder of `toolbox:view` or `toolbox:manage`, so `access` in the response is always `owner`, `view`, `use` or `edit`. `?include=schemas` opts into full JSON Schemas on `tools[].input_schema`; it is 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, shareable 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."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Number of proxy entries with no connection pinned (`connection_id` is null), which is what a \"N need connection\" status is built from. Excludes synthetic entries, which pin no `connection_id` of their own by design. Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in the detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, summarized as at most a handful of connectors. Present on every `GET /toolbox` row: a stored row summarizes its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarizes the connections it spans. Also present on every command response (`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-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, so 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.","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` or `use` grantee, because who else has access is information about colleagues. Read `can_see_shares` to tell \"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 short preview of who the toolbox is shared with (same cap as `access_summary`). Present only on `GET /toolbox/{id}` when the caller may see who it is shared with. Never on a list row (`GET /toolbox`); page `GET /toolbox/{id}/share` for the full, cursor-paginated list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a short summary. Present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see who it is shared with (the same condition as `access_summary`). 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."}}}}}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response. Omitted on a dynamic row, which has no skill of its own."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill. Omitted on a dynamic row."},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this toolbox. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the toolbox reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Set by the server, never a client input: whose `use` grant on `connection_id` this pin rides on. If this user loses `use` on the connection, the entry goes unmet for every executor, not just them. Null if and only if `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`."},"unmet_reason":{"type":["string","null"],"enum":["connector_not_shared",null],"description":"Detail responses only: the one `unmet` cause worth naming, null for every other. `connector_not_shared`: the pinned connection is live and delegation-intact, but its OWNER no longer holds `use` on the org-owned connector it runs through, so the entry is listed (flagged) and never advertised, and every call through it is refused. Re-sharing the connector clears it with no edit to the toolbox. Not repairable by re-pinning — `can_repin` stays false for it."},"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`, using the same precedence as the caller's own `access_via` (owner, else org, else team, else direct; the broadest wins). `delegation_ok` says the delegation still stands; this says what it stands on, so a client 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 recorded delegator, and whenever the delegation no longer works (`delegation_ok: false`): the delegator is no longer an active member, or their access has fallen below `use`."},"can_repin":{"type":"boolean","description":"Detail responses only, and the only field here that differs by reader: whether the caller may 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."},"can_view_connector":{"type":"boolean","description":"Present on the toolbox's own responses, per reader: whether the caller may open `connector_slug` (`GET /connector/{slug}` and its `/tools`). False when the entry is pinned to an organization-owned custom connector that nobody has shared with the caller; those routes answer 404 for them. The entry still carries its label and logo and still runs on its delegator's access; this only says the connector page and its tool picker are not for this reader. Always true for a platform connector and for a synthetic entry."},"tool_title":{"type":["string","null"],"description":"Response-only. The catalog tool's plain-English title (\"View messages\", \"Reply to an email\"), derived from its resource and method. Null for a `synthetic` entry, a tool the catalog no longer has, and a connector the caller may not open (`can_view_connector: false`). Render `overrides.name`, else this, else `tool_name`."},"connector":{"type":["object","null"],"description":"Response-only. The connector’s label and logo; null for a `synthetic` entry. A connector the catalog cannot resolve reads its slug as `label` and a null `logo`.","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]},"upstream_slug":{"type":"string","description":"Only on a fork whose upstream the caller may see, by the same rule as `GET /connection`: omitted when the caller has no access to the fork or the upstream is hidden from them, and when the upstream cannot be read."}}},"synthetic_tool_name":{"type":["string","null"],"description":"Response-only. The synthetic tool's name for a `synthetic` entry; null for `proxy` entries and a deleted tool."},"delegated_by_label":{"type":["string","null"],"description":"Response-only. The name (else email) behind `delegated_by_user_id`. Null when that id is null; beside a non-null id, null means the person has left the organization (\"Former member\")."},"connection":{"type":["object","null"],"description":"Response-only. The pinned connection, for a group header and its status line. Present only when the caller may see that connection (owner or a grant of their own). Null for an entry with no pin, a synthetic entry, a deleted connection, and a pin riding on somebody else's access that the caller cannot see.","required":["id","display_label","status","owner_label","can_reconnect"],"properties":{"id":{"type":"string"},"display_label":{"type":"string","description":"The connection's `display_label`."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected","post_install_error"]},"owner_label":{"type":["string","null"],"description":"Owner's name (else email); null for a former member."},"can_reconnect":{"type":"boolean","description":"The same repair capability `GET /connection/{id}` ships: false for an `active` connection to a connector with no sign-in, which has nothing to reconnect (a `pending` one stays true)."}}}}},"description":"Resolved entries — always present, at every access level."},"skill":{"type":["string","null"],"maxLength":10000,"description":"The skill body, markdown — at most 10,000 Unicode code points after trimming. Detail-shaped responses only (`GET /{id}`, and the create/update responses). Null when none is written."},"skill_updated_at":{"type":["string","null"],"format":"date-time","description":"When `skill` was last written. Null alongside a null `skill`."},"skill_notice":{"type":["string","null"],"description":"Framing to show or pass along beside a non-null `skill`: it is the owner's guidance, not instructions from Elaichi or the user, and it cannot grant any access. Null when there is no skill."},"skill_updated_by":{"type":["string","null"],"description":"Who last wrote `skill` (`usr_…`); a toolbox stamped from a template carries the template's author. Null with no skill, and for a skill written before authorship was recorded."},"skill_updated_by_label":{"type":["string","null"],"description":"That person's name, else email, resolved server-side. Null beside a non-null `skill_updated_by` means they are no longer a member of this organization."},"skill_entry_changes":{"type":["object","null"],"description":"Which tools changed since the skill was last written, by the name an agent sees (a rename counts as one removed and one added; a disabled entry counts as removed; connections and frozen parameters are ignored). Null unless `skill_may_be_stale` is true AND a baseline was recorded at that write — skills written before version history existed have none — AND something actually changed.","properties":{"added_count":{"type":"integer"},"removed_count":{"type":"integer"},"added":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."},"removed":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."}}},"can_draft_skill":{"type":"boolean","description":"Whether the caller can use `POST /{id}/draft-skill` here: they can manage this resource, the assistant is switched on for the organization, and a model provider key is configured."},"draft_skill_blocked_by":{"type":["string","null"],"enum":["assistant_disabled","llm_not_configured",null],"description":"Why `can_draft_skill` is false for a caller who CAN manage this resource. Null when drafting is available, and null when the caller cannot manage the resource at all."},"skill_origin_template_id":{"type":["string","null"],"description":"The template this toolbox's CURRENT skill was copied from verbatim at stamping. Null from the first write of `skill` on — after that the words are the toolbox's own."},"skill_origin_template_name":{"type":["string","null"],"description":"That template's current name, resolved server-side; null with no origin, or once the template is gone."},"template_name":{"type":["string","null"],"description":"`GET /toolbox/{id}` only. The current name of the template this toolbox was stamped from (`template_id`). Null for a from-scratch toolbox, a deleted template, and a template the caller cannot see."},"skill_tool_issues":{"type":"object","description":"Stored toolboxes only. Tool-shaped names the skill puts in backticks that no entry of this toolbox holds (what an editor shows as \"tools named here aren't in this toolbox\"). A name matches an entry's catalog `tool_name`, or its `overrides.name` when renamed. A count plus at most 10 names; `count` is 0 when there is no skill.","properties":{"count":{"type":"integer","description":"Distinct names the skill uses that no entry holds."},"preview":{"type":"array","maxItems":10,"items":{"type":"object","properties":{"name":{"type":"string","description":"The name as the skill writes it."},"suggestion":{"type":["string","null"],"description":"The entry's own name this most likely means (`list_query_database` -> `list_all_notion_query_database`); null unless exactly one entry matches."}}}}}},"skill_agent_preview":{"type":["object","null"],"description":"Stored toolboxes only: what an agent connected to this toolbox is shown before it reads the skill, built by the same code the MCP endpoint uses. `instructions_line` is the line the server instructions carry for a connection limited to chosen toolboxes; `resource` is the `elaichi://toolbox/{id}/skill` entry in `resources/list`. The resource text itself is `skill_notice`, a blank line, then `skill`. Null with no skill.","properties":{"instructions_line":{"type":"string"},"resource":{"type":"object","properties":{"uri":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"mime_type":{"type":"string"}}}}},"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, labeled 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. 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. This is a summary, never the full set, so the response stays the same size however many connections the toolbox spans. 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 or share response it can be `null`: the write succeeded but the summary could not be computed (this is not the same as \"nothing is delegated\", which is an all-zero summary); re-read `GET /toolbox/{id}`. 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. Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection that `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"},"logo":{"type":["string","null"],"description":"The connector's logo."},"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 recorded delegator (an older entry), so `delegated_by` is the connection's owner standing in as the delegator. 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, with the same precedence as `access_via`: 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, a client reads an org-wide or team grant as a personal favor 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 access to the connection has fallen below `use`), since the only honest statement left is who pinned it."},"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. Absent otherwise, and a client should then say \"a team\" without naming it, because the team's name is not the caller's to learn.","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":"Updates a toolbox. **`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 an existing row keeps that row's original delegator rather than re-stamping whoever clicked Save; only genuinely new pairs are checked against the editor's own access and stamped with them. Entries keep their array order, and a row sent back with its own `id` keeps that id; every other row gets a fresh one. Omitting `entries` leaves them untouched. `name` and `description` are ordinary merge-patch fields, and so is `skill`: a write clears `skill_may_be_stale` and `skill_origin_template_id`, and an `entries` write sets `skill_may_be_stale` when a skill exists and the same request did not also write `skill`. Every skill edit is audited on `toolbox.updated` (`metadata.skill` is `written`, `reaffirmed` or `cleared`, plus `metadata.skill_version_id`, never the text) and kept in `GET /toolbox/{id}/skill-version`; a `view` or `use` grantee reads the skill and gets `403` writing it. Returns the same resolved shape `GET /toolbox/{id}` does. Rejects an automatic id with 400, since there is no row to update. Requires `edit` access or ownership (a grant of the caller's own); `toolbox:manage` alone is not enough.","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"},"id":{"type":["string","null"],"maxLength":200,"description":"Optional identity hint (`tbxe_…` on a toolbox, `tple_…` on a template): the `id` of a row this resource already has. On `PATCH /toolbox/{id}` and `PATCH /template/{id}` a row echoed back with its own id keeps it, so a client that keys per-row state by entry id (a switch mid-save) is not orphaned by the replacement. It is a hint, never an assertion: an id that is not one of THIS resource's current rows, is repeated, or would put the row out of stored order (rows list in id order) is ignored and the row gets a fresh id — and so is a value that is not a usable string (not a string, or over 200 characters). Never a 400, and never adopted from another resource. Omit it for a new row. Ignored on create."},"connection_id":{"type":["string","null"],"description":"Pin this entry to one connection (`conn_…`) the editor making the request can use; they become its delegator. `null` = \"needs connection.\" On `PATCH /toolbox/{id}`, an incoming entry whose (connection_id, tool_name) pair matches an existing row keeps that row's original delegator rather than re-stamping the caller; only genuinely new pairs get the usability check and the caller's own stamp."}}},"description":"Replaces every entry."},"skill":{"type":["string","null"],"maxLength":10000,"description":"Markdown guidance on how to use these tools well. Trimmed, then capped at 10,000 Unicode code points (an emoji counts once) — longer is a `400` naming the limit, never a silent cut. `null` or an empty string clears it; omit the key to leave it unchanged. The resource's `description` is the one-line \"when to use this\" shown beside it, so write that too. Moderated like the description."}}}}}},"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":"Deletes a toolbox. 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, shareable 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."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Number of proxy entries with no connection pinned (`connection_id` is null), which is what a \"N need connection\" status is built from. Excludes synthetic entries, which pin no `connection_id` of their own by design. Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in the detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, summarized as at most a handful of connectors. Present on every `GET /toolbox` row: a stored row summarizes its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarizes the connections it spans. Also present on every command response (`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-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, so 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.","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` or `use` grantee, because who else has access is information about colleagues. Read `can_see_shares` to tell \"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 short preview of who the toolbox is shared with (same cap as `access_summary`). Present only on `GET /toolbox/{id}` when the caller may see who it is shared with. Never on a list row (`GET /toolbox`); page `GET /toolbox/{id}/share` for the full, cursor-paginated list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a short summary. Present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see who it is shared with (the same condition as `access_summary`). 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."}}}}}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response. Omitted on a dynamic row, which has no skill of its own."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill. Omitted on a dynamic row."},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this toolbox. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the toolbox reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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":"Set by the server, never a client input: whose `use` grant on `connection_id` this pin rides on. If this user loses `use` on the connection, the entry goes unmet for every executor, not just them. Null if and only if `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`."},"unmet_reason":{"type":["string","null"],"enum":["connector_not_shared",null],"description":"Detail responses only: the one `unmet` cause worth naming, null for every other. `connector_not_shared`: the pinned connection is live and delegation-intact, but its OWNER no longer holds `use` on the org-owned connector it runs through, so the entry is listed (flagged) and never advertised, and every call through it is refused. Re-sharing the connector clears it with no edit to the toolbox. Not repairable by re-pinning — `can_repin` stays false for it."},"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`, using the same precedence as the caller's own `access_via` (owner, else org, else team, else direct; the broadest wins). `delegation_ok` says the delegation still stands; this says what it stands on, so a client 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 recorded delegator, and whenever the delegation no longer works (`delegation_ok: false`): the delegator is no longer an active member, or their access has fallen below `use`."},"can_repin":{"type":"boolean","description":"Detail responses only, and the only field here that differs by reader: whether the caller may 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."},"can_view_connector":{"type":"boolean","description":"Present on the toolbox's own responses, per reader: whether the caller may open `connector_slug` (`GET /connector/{slug}` and its `/tools`). False when the entry is pinned to an organization-owned custom connector that nobody has shared with the caller; those routes answer 404 for them. The entry still carries its label and logo and still runs on its delegator's access; this only says the connector page and its tool picker are not for this reader. Always true for a platform connector and for a synthetic entry."},"tool_title":{"type":["string","null"],"description":"Response-only. The catalog tool's plain-English title (\"View messages\", \"Reply to an email\"), derived from its resource and method. Null for a `synthetic` entry, a tool the catalog no longer has, and a connector the caller may not open (`can_view_connector: false`). Render `overrides.name`, else this, else `tool_name`."},"connector":{"type":["object","null"],"description":"Response-only. The connector’s label and logo; null for a `synthetic` entry. A connector the catalog cannot resolve reads its slug as `label` and a null `logo`.","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]},"upstream_slug":{"type":"string","description":"Only on a fork whose upstream the caller may see, by the same rule as `GET /connection`: omitted when the caller has no access to the fork or the upstream is hidden from them, and when the upstream cannot be read."}}},"synthetic_tool_name":{"type":["string","null"],"description":"Response-only. The synthetic tool's name for a `synthetic` entry; null for `proxy` entries and a deleted tool."},"delegated_by_label":{"type":["string","null"],"description":"Response-only. The name (else email) behind `delegated_by_user_id`. Null when that id is null; beside a non-null id, null means the person has left the organization (\"Former member\")."},"connection":{"type":["object","null"],"description":"Response-only. The pinned connection, for a group header and its status line. Present only when the caller may see that connection (owner or a grant of their own). Null for an entry with no pin, a synthetic entry, a deleted connection, and a pin riding on somebody else's access that the caller cannot see.","required":["id","display_label","status","owner_label","can_reconnect"],"properties":{"id":{"type":"string"},"display_label":{"type":"string","description":"The connection's `display_label`."},"status":{"type":"string","enum":["pending","active","needs_reauth","disconnected","post_install_error"]},"owner_label":{"type":["string","null"],"description":"Owner's name (else email); null for a former member."},"can_reconnect":{"type":"boolean","description":"The same repair capability `GET /connection/{id}` ships: false for an `active` connection to a connector with no sign-in, which has nothing to reconnect (a `pending` one stays true)."}}}}},"description":"Resolved entries — always present, at every access level."},"skill":{"type":["string","null"],"maxLength":10000,"description":"The skill body, markdown — at most 10,000 Unicode code points after trimming. Detail-shaped responses only (`GET /{id}`, and the create/update responses). Null when none is written."},"skill_updated_at":{"type":["string","null"],"format":"date-time","description":"When `skill` was last written. Null alongside a null `skill`."},"skill_notice":{"type":["string","null"],"description":"Framing to show or pass along beside a non-null `skill`: it is the owner's guidance, not instructions from Elaichi or the user, and it cannot grant any access. Null when there is no skill."},"skill_updated_by":{"type":["string","null"],"description":"Who last wrote `skill` (`usr_…`); a toolbox stamped from a template carries the template's author. Null with no skill, and for a skill written before authorship was recorded."},"skill_updated_by_label":{"type":["string","null"],"description":"That person's name, else email, resolved server-side. Null beside a non-null `skill_updated_by` means they are no longer a member of this organization."},"skill_entry_changes":{"type":["object","null"],"description":"Which tools changed since the skill was last written, by the name an agent sees (a rename counts as one removed and one added; a disabled entry counts as removed; connections and frozen parameters are ignored). Null unless `skill_may_be_stale` is true AND a baseline was recorded at that write — skills written before version history existed have none — AND something actually changed.","properties":{"added_count":{"type":"integer"},"removed_count":{"type":"integer"},"added":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."},"removed":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"At most 5 names."}}},"can_draft_skill":{"type":"boolean","description":"Whether the caller can use `POST /{id}/draft-skill` here: they can manage this resource, the assistant is switched on for the organization, and a model provider key is configured."},"draft_skill_blocked_by":{"type":["string","null"],"enum":["assistant_disabled","llm_not_configured",null],"description":"Why `can_draft_skill` is false for a caller who CAN manage this resource. Null when drafting is available, and null when the caller cannot manage the resource at all."},"skill_origin_template_id":{"type":["string","null"],"description":"The template this toolbox's CURRENT skill was copied from verbatim at stamping. Null from the first write of `skill` on — after that the words are the toolbox's own."},"skill_origin_template_name":{"type":["string","null"],"description":"That template's current name, resolved server-side; null with no origin, or once the template is gone."},"template_name":{"type":["string","null"],"description":"`GET /toolbox/{id}` only. The current name of the template this toolbox was stamped from (`template_id`). Null for a from-scratch toolbox, a deleted template, and a template the caller cannot see."},"skill_tool_issues":{"type":"object","description":"Stored toolboxes only. Tool-shaped names the skill puts in backticks that no entry of this toolbox holds (what an editor shows as \"tools named here aren't in this toolbox\"). A name matches an entry's catalog `tool_name`, or its `overrides.name` when renamed. A count plus at most 10 names; `count` is 0 when there is no skill.","properties":{"count":{"type":"integer","description":"Distinct names the skill uses that no entry holds."},"preview":{"type":"array","maxItems":10,"items":{"type":"object","properties":{"name":{"type":"string","description":"The name as the skill writes it."},"suggestion":{"type":["string","null"],"description":"The entry's own name this most likely means (`list_query_database` -> `list_all_notion_query_database`); null unless exactly one entry matches."}}}}}},"skill_agent_preview":{"type":["object","null"],"description":"Stored toolboxes only: what an agent connected to this toolbox is shown before it reads the skill, built by the same code the MCP endpoint uses. `instructions_line` is the line the server instructions carry for a connection limited to chosen toolboxes; `resource` is the `elaichi://toolbox/{id}/skill` entry in `resources/list`. The resource text itself is `skill_notice`, a blank line, then `skill`. Null with no skill.","properties":{"instructions_line":{"type":"string"},"resource":{"type":"object","properties":{"uri":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"mime_type":{"type":"string"}}}}},"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, labeled 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. 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. This is a summary, never the full set, so the response stays the same size however many connections the toolbox spans. 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 or share response it can be `null`: the write succeeded but the summary could not be computed (this is not the same as \"nothing is delegated\", which is an all-zero summary); re-read `GET /toolbox/{id}`. 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. Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection that `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"},"logo":{"type":["string","null"],"description":"The connector's logo."},"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 recorded delegator (an older entry), so `delegated_by` is the connection's owner standing in as the delegator. 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, with the same precedence as `access_via`: 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, a client reads an org-wide or team grant as a personal favor 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 access to the connection has fallen below `use`), since the only honest statement left is who pinned it."},"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. Absent otherwise, and a client should then say \"a team\" without naming it, because the team's name is not the caller's to learn.","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. It works 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. Requires `edit` (or ownership) on the toolbox and `use` on the connection, the same requirement 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 an automatic 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, shareable 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."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"entry_count":{"type":"integer","description":"Omitted for dynamic rows, which have no stored entries of their own."},"needs_connection_count":{"type":"integer","description":"Number of proxy entries with no connection pinned (`connection_id` is null), which is what a \"N need connection\" status is built from. Excludes synthetic entries, which pin no `connection_id` of their own by design. Omitted for dynamic rows (nothing to bind). Broken delegation (`delegation_ok: false`) only surfaces in the detail, inside `entries[]`."},"connectors":{"type":"object","description":"Which integrations this toolbox's tools come from, summarized as at most a handful of connectors. Present on every `GET /toolbox` row: a stored row summarizes its own entries, a `connection:{id}` row is that connection's one connector, and a `global:{user_id}` row summarizes the connections it spans. Also present on every command response (`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-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, so 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.","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` or `use` grantee, because who else has access is information about colleagues. Read `can_see_shares` to tell \"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 short preview of who the toolbox is shared with (same cap as `access_summary`). Present only on `GET /toolbox/{id}` when the caller may see who it is shared with. Never on a list row (`GET /toolbox`); page `GET /toolbox/{id}/share` for the full, cursor-paginated list.","items":{"$ref":"#/components/schemas/AclEntry"}},"connected_app_summary":{"type":"object","description":"The OAuth-connected apps that reach this toolbox, as a short summary. Present only on `GET /toolbox/{id}` for a stored toolbox, and only when the caller may see who it is shared with (the same condition as `access_summary`). 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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."}}}}}},"has_skill":{"type":"boolean","description":"Whether a skill is written. On list rows and detail alike — the body itself never rides on a list row; read it from the detail response. Omitted on a dynamic row, which has no skill of its own."},"skill_may_be_stale":{"type":"boolean","description":"True once the entries changed after the skill was last written; cleared by the next write of `skill`. Never true without a skill. Omitted on a dynamic row."},"can_use":{"type":"boolean","description":"Whether the caller may execute this toolbox's tools — based on the caller's ownership or grant level. Present on every list and detail row for both templates and toolboxes."},"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":"Whether the caller may grant or revoke access to this toolbox. It matches `can_share` and is separate from `can_manage`: an `edit` grantee whose role omits `toolbox:share` can edit the toolbox but cannot change who else has access. 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 names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the toolbox reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `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 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":"Share id (`acl_…`), the `{aclId}` that `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"]},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."},"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":"Lists who a toolbox is shared with. 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":{"oneOf":[{"allOf":[{"$ref":"#/components/schemas/AclEntry"},{"type":"object","properties":{"delegation":{"type":["object","null"],"description":"What this toolbox runs on. 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. This is a summary, never the full set, so the response stays the same size however many connections the toolbox spans. 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 or share response it can be `null`: the write succeeded but the summary could not be computed (this is not the same as \"nothing is delegated\", which is an all-zero summary); re-read `GET /toolbox/{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. Every label and delegator name lives in here, so a `use` grantee is never told the name or owner of a connection that `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"},"logo":{"type":["string","null"],"description":"The connector's logo."},"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 recorded delegator (an older entry), so `delegated_by` is the connection's owner standing in as the delegator. 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, with the same precedence as `access_via`: 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, a client reads an org-wide or team grant as a personal favor 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 access to the connection has fallen below `use`), since the only honest statement left is who pinned it."},"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. Absent otherwise, and a client should then say \"a team\" without naming it, because the team's name is not the caller's to learn.","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."}}}}}}}}}}]},{"$ref":"#/components/schemas/ShareManyResponse"}]}}}},"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 organization access to a toolbox 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 access, never their own. Re-granting the same grantee replaces the level rather than stacking. The response repeats the toolbox's `delegation` summary (the same short summary `GET /toolbox/{id}` carries), so a client that shares without ever having read the detail still learns what it just handed out. Rejects an automatic id with 400, since nothing is shareable there. Requires `edit` access or ownership, and `toolbox:share`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ShareRequest"},{"$ref":"#/components/schemas/ShareManyRequest"}]}}}},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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":"Lists the OAuth-connected apps (MCP clients) that reach this toolbox, cursor-paginated. Visible to the same people as `GET /toolbox/{id}/share`, because an app reaching a toolbox is 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 (checked live against the grantee's own access, so it cannot go stale). `is_own` flags the caller's own authorization. Requires owner or `edit` access (a grant of the caller's own); `toolbox:manage` alone is not enough.","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}/skill-version":{"get":{"operationId":"listToolboxSkillVersions","summary":"List a toolbox's skill versions","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":"Version id (`skv_…`)."},"kind":{"type":"string","enum":["written","reaffirmed","cleared","copied"],"description":"`written`: new text. `reaffirmed`: the same text saved again (\"still accurate\"). `cleared`: the skill was removed. `copied`: a toolbox stamped from a template started with its skill."},"excerpt":{"type":["string","null"],"description":"The opening of the text as one plain line, at most 200 code points. Null for `cleared`."},"char_count":{"type":"integer","description":"Length in Unicode code points; 0 for `cleared`."},"author":{"type":["string","null"],"description":"Who made this write (`usr_…`)."},"author_label":{"type":["string","null"],"description":"Their name, else email. Null beside a non-null `author`: no longer a member."},"origin_template_id":{"type":["string","null"],"description":"`copied` rows: the template it came from."},"origin_template_name":{"type":["string","null"],"description":"Its current name; null once deleted."},"created_at":{"type":"string","format":"date-time"},"is_current":{"type":"boolean","description":"True on the newest version — the text the resource holds now."},"matches_current":{"type":"boolean","description":"True when this version's text equals the resource's current skill (restoring it would change nothing). Not the same as `is_current`: a later \"still accurate\" save repeats an older version's text, and both match."}}}},"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 write of the toolbox's `skill`, newest first, cursor-paginated — the 50 newest are kept per toolbox, older ones are pruned on the next write. Restoring a version is an ordinary `PATCH /toolbox/{id} { skill }` with its text, which records a new version. Requires `edit` access or ownership: `404` when the caller cannot see the toolbox, `403` when they can see it below `edit`. Rejects a dynamic id with `400`.","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}/skill-version/{versionId}":{"get":{"operationId":"getToolboxSkillVersion","summary":"Get one toolbox skill version with its text","tags":["Toolboxes"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"allOf":[{"type":"object","properties":{"id":{"type":"string","description":"Version id (`skv_…`)."},"kind":{"type":"string","enum":["written","reaffirmed","cleared","copied"],"description":"`written`: new text. `reaffirmed`: the same text saved again (\"still accurate\"). `cleared`: the skill was removed. `copied`: a toolbox stamped from a template started with its skill."},"excerpt":{"type":["string","null"],"description":"The opening of the text as one plain line, at most 200 code points. Null for `cleared`."},"char_count":{"type":"integer","description":"Length in Unicode code points; 0 for `cleared`."},"author":{"type":["string","null"],"description":"Who made this write (`usr_…`)."},"author_label":{"type":["string","null"],"description":"Their name, else email. Null beside a non-null `author`: no longer a member."},"origin_template_id":{"type":["string","null"],"description":"`copied` rows: the template it came from."},"origin_template_name":{"type":["string","null"],"description":"Its current name; null once deleted."},"created_at":{"type":"string","format":"date-time"},"is_current":{"type":"boolean","description":"True on the newest version — the text the resource holds now."},"matches_current":{"type":"boolean","description":"True when this version's text equals the resource's current skill (restoring it would change nothing). Not the same as `is_current`: a later \"still accurate\" save repeats an older version's text, and both match."}}},{"type":"object","properties":{"skill":{"type":["string","null"],"description":"The full text of this version; null for `cleared`."}}}]}}}},"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":"Same access rule as the list. A version id that belongs to a different toolbox is a `404`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Stored toolbox id (`tbx_…`)."},{"name":"versionId","in":"path","required":true,"schema":{"type":"string"},"description":"Version id (`skv_…`)."}]}},"/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":"Revokes a share on a toolbox. 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":"Share id (`acl_…`) 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":"Lists synthetic tools (`syn_…`): 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 }`) alongside `owner_user_id`; read it rather than resolving the id against the paginated member directory. Each row also carries `broken_step_count`, the number of steps whose connection has been deleted (0 = none).","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`. The response annotates each step the way `GET /synthetic-tool/{id}` does (`connection_state`, `connection_name`).","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/validate":{"post":{"operationId":"validateSyntheticTool","summary":"Validate a synthetic tool draft without saving it","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"}}}},"413":{"description":"Request body over 4 MiB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Same body shape as POST /synthetic-tool, checked as a new tool. Always 200 with every problem found, errors first -- `{ issues: (severity, step, field, message)[] }`, at most 50 -- including connection and restriction problems that POST answers with 404/403. Any error-severity issue means POST would reject it. Not checked: the name/description language check, a name already in use, and PATCH's exemption for unchanged steps. Bodies over 4 MiB are 413.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","description","steps"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"input_schema":{"type":"object","additionalProperties":true},"steps":{"type":"array","items":{"type":"object","additionalProperties":true}},"output_template":{"type":["string","null"]}}}}}},"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"}}}}},"description":"Each step carries read-only `connection_state`, computed on read: `ok` (the connection exists and the caller can see it; `connection_name` is its display name), `missing` (the connection was deleted; the step keeps its `connection_id` and cannot run until it is pointed at another connection) or `unavailable` (the connection exists but the caller cannot see it, so it is not named). A step with no `connection_id` has neither field. Request bodies never accept these; they are stripped if echoed back. `broken_step_count` (steps whose state is `missing`) rides along, as on the list.","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":"Updates a synthetic tool. 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. The response annotates each step with `connection_state` / `connection_name`, same as `GET /synthetic-tool/{id}`.","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":"Deletes a synthetic tool. 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":"Send a JSON-RPC request to the MCP server","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":"Handles JSON-RPC requests from an MCP client. 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 (under \"All my tools\", every tool of every connection they can use, resolved through their virtual `global:{user_id}` toolbox, plus every toolbox shared with them at `use` or above) 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 honored.\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 them. 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":"Reject this method on the MCP endpoint","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"}}}}},"description":"Answers `405` with `Allow: POST`. The MCP endpoint is POST-only: there is no server-sent event stream to open with GET.","security":[]},"delete":{"operationId":"mcpControlPlaneDeleteNotAllowed","summary":"Reject this method on the MCP endpoint","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"}}}}},"description":"Answers `405` with `Allow: POST`. The MCP endpoint is POST-only: there is no session to end with DELETE.","security":[]}},"/.well-known/oauth-protected-resource":{"get":{"operationId":"getOAuthProtectedResourceMetadata","summary":"Get 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":"Get 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":"Get the OpenID Connect discovery document","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 in Elaichi; `mcp:tools` runs the third-party tools the user has connected, deletes included. `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":"Get user info (OIDC)","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":"Get user info (OIDC)","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. A member is bound by their role's rules and their own `allow`/`block` rules at once; a connector or tool is reachable only when both admit it. `except` no longer appears here: the way to lift a role rule for one member is an approved access grant (`POST /access-request/{id}/resolve`), not a rule you write or read through this endpoint."},"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 /restriction/connector/{slug}/tools`."},"tool_label":{"type":"string","description":"Person-readable phrase for this tool (\"create a contact\"), generated from `tool_name`. Read-only. Show this instead of the raw id. 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.\"), so every client says the same thing. Read-only, derived from `mode`, `connector_slugs` and `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. Show this rather than composing your own."},"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. `null` while `created_by` is a real id means the author has left the org; show 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`, with the same \"Former member\" meaning for a null beside a real id. To decide whether somebody other than the author last wrote the row, compare the two ids, never the two labels: two people can share a display name, and both labels are null once both have left."},"legacy_override":{"type":"boolean","description":"True on a member row that still REPLACES its member's role rules — the meaning every member row had before role and member rules combined, kept on the rows whose conversion could not be proven to leave the member's access unchanged (a role allowlist beside member rows with none). While any of a member's rows carries it, no role rule reaches them, including ones added later. Read-only except that `PATCH` may send `false` to clear it on all of that member's rows, which only ever narrows them."},"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. Always `null` on a `role` target (look the role up by `target_id` in `GET /role`), and `null` on a stale target whose member has been deleted."},"connector_labels":{"type":"object","additionalProperties":{"type":"string"},"description":"Slug to catalog display name for the first 8 connectors the rule enforces (whole connectors first, then tool-only ones), enough for a row's badges and hover text. A slug the catalog could not name (deleted, or the catalog unreachable) is absent: fall back to the slug."},"scope_connectors":{"type":"object","additionalProperties":{"type":"object","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]}}},"description":"One-row responses only (`GET /restriction/{id}`, `POST /restriction`, `PATCH /restriction/{id}`; never a list row): label and logo for every connector the rule scopes (its whole connectors and the connectors its tools belong to), keyed by slug. At most 1000 connectors plus the connectors of 1000 tools. A slug the catalog cannot resolve reads its slug as `label` and a null `logo`."},"can_manage":{"type":"boolean","description":"Whether the caller may create, edit or delete this row: `restriction:manage`, plus two extra checks the write routes 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. A row showing `can_manage: true` will not 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":"Lists restrictions. Requires `restriction:view`. A restriction limits which connectors and tools a role or member may reach, and every surface applies it: MCP clients, toolboxes and `GET /connection/{id}/tools`.","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. A member is bound by their role's rules and their own `allow`/`block` rules at once; a connector or tool is reachable only when both admit it. `except` no longer appears here: the way to lift a role rule for one member is an approved access grant (`POST /access-request/{id}/resolve`), not a rule you write or read through this endpoint."},"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 /restriction/connector/{slug}/tools`."},"tool_label":{"type":"string","description":"Person-readable phrase for this tool (\"create a contact\"), generated from `tool_name`. Read-only. Show this instead of the raw id. 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.\"), so every client says the same thing. Read-only, derived from `mode`, `connector_slugs` and `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. Show this rather than composing your own."},"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. `null` while `created_by` is a real id means the author has left the org; show 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`, with the same \"Former member\" meaning for a null beside a real id. To decide whether somebody other than the author last wrote the row, compare the two ids, never the two labels: two people can share a display name, and both labels are null once both have left."},"legacy_override":{"type":"boolean","description":"True on a member row that still REPLACES its member's role rules — the meaning every member row had before role and member rules combined, kept on the rows whose conversion could not be proven to leave the member's access unchanged (a role allowlist beside member rows with none). While any of a member's rows carries it, no role rule reaches them, including ones added later. Read-only except that `PATCH` may send `false` to clear it on all of that member's rows, which only ever narrows them."},"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. Always `null` on a `role` target (look the role up by `target_id` in `GET /role`), and `null` on a stale target whose member has been deleted."},"connector_labels":{"type":"object","additionalProperties":{"type":"string"},"description":"Slug to catalog display name for the first 8 connectors the rule enforces (whole connectors first, then tool-only ones), enough for a row's badges and hover text. A slug the catalog could not name (deleted, or the catalog unreachable) is absent: fall back to the slug."},"scope_connectors":{"type":"object","additionalProperties":{"type":"object","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]}}},"description":"One-row responses only (`GET /restriction/{id}`, `POST /restriction`, `PATCH /restriction/{id}`; never a list row): label and logo for every connector the rule scopes (its whole connectors and the connectors its tools belong to), keyed by slug. At most 1000 connectors plus the connectors of 1000 tools. A slug the catalog cannot resolve reads its slug as `label` and a null `logo`."},"can_manage":{"type":"boolean","description":"Whether the caller may create, edit or delete this row: `restriction:manage`, plus two extra checks the write routes 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. A row showing `can_manage: true` will not 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":"Creates a restriction. Requires `restriction:manage` and the `restrictions` plan feature; a `user` target additionally requires `restriction:override`. A member is bound by their role's rules and their own at once, so a member's `allow` or `block` rule only ever narrows. `mode: \"except\"` is `400`: the one way to loosen a role rule for one member is an approved access grant (`POST /access-request/{id}/resolve`), which records who asked, who approved it and, optionally, when it expires. A rule aimed at yourself is always `403`, even when nothing restricts you. Changes take effect within about a minute.\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 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 /restriction/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/connector/{slug}/tools":{"get":{"operationId":"listRestrictionCatalogTools","summary":"List a connector’s tools for writing a restriction","tags":["Restrictions"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor","operation_counts"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Exact tool name — what `tools[].tool_name` must contain."},"tool_label":{"type":"string","description":"The same phrase a saved rule’s `tools[].tool_label` carries for this tool, so a tool reads the same in the picker as on the saved rule."},"description":{"type":"string"},"resource":{"type":"string"},"method":{"type":"string"},"operation":{"type":"string","enum":["read","create","update","delete","other"],"description":"Same CRUD bucket as on `GET /connector/{slug}/tools`. Filter with `?operation=`."},"title":{"type":"string","description":"The remote MCP server's human title for the tool, as on `GET /connector/{slug}/tools`. Present only on a remote MCP tool that has one."},"tier":{"type":"string","enum":["read","write","destructive"],"description":"Always present, as on `GET /connector/{slug}/tools`: how the approval gate and the MCP scope ladder classify this tool. For a remote MCP tool, the server’s label. Read `kind` to tell a remote MCP tool, never this field."},"kind":{"type":"string","enum":["catalog","remote_mcp"],"description":"`remote_mcp` for a tool from an organization's remote MCP connector, else `catalog`."},"status":{"type":"string","enum":["active","disabled"],"description":"Whether a remote MCP tool is on in the organization's registry (`disabled`: a manager turned it off). Present only on a remote MCP tool, where `operation_counts` is not returned; absent on a catalog tool."}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]},"operation_counts":{"type":"object","required":["read","create","update","delete","other"],"properties":{"read":{"type":"integer"},"create":{"type":"integer"},"update":{"type":"integer"},"delete":{"type":"integer"},"other":{"type":"integer"}},"description":"How many tools of each CRUD bucket the `q` filter alone leaves — never narrowed further by `operation` itself, so the count beside an unselected filter chip stays honest. Repeated on every page."}}}}}},"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":"Lists every tool a connector defines (name, person-readable label, description, resource and method), for picking `tools[].tool_name` values in a restriction. Unlike `GET /connector/{slug}/tools`, this answers regardless of the caller’s own restrictions: that route returns `403` for a connector the caller is blocked from, which would leave a member unable to narrow a rule that blocks them to specific tools. Reading the catalog grants nothing: execution, connections and toolbox pins still apply the caller’s restrictions, and the write routes still decide whether the caller may write the rule. Requires `restriction:manage` and `connector:view` (browsing the catalog needs the latter everywhere; this route drops only the caller’s use-side restriction). Cursor-paged by `name`. For an organization’s remote MCP connector the rows are the org’s registry of tool names (turned-off ones included, with `status`, `tier` and `title`), `resource` is `mcp`, `method` is the upstream tool name, and `operation_counts` is not returned.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"slug","in":"path","required":true,"schema":{"type":"string"},"description":"Connector slug."},{"$ref":"#/components/parameters/LimitQuery"},{"$ref":"#/components/parameters/CursorQuery"},{"name":"q","in":"query","required":false,"description":"Case-insensitive substring match on `tool_label`, `name`, `description` and `resource`. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"operation","in":"query","required":false,"description":"Comma-separated CRUD buckets to keep: read, create, update, delete, other. Unknown values 400. Applied before paging, so the cursor never counts a row the filter would have hidden.","schema":{"type":"string"}}]}},"/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. A member is bound by their role's rules and their own `allow`/`block` rules at once; a connector or tool is reachable only when both admit it. `except` no longer appears here: the way to lift a role rule for one member is an approved access grant (`POST /access-request/{id}/resolve`), not a rule you write or read through this endpoint."},"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 /restriction/connector/{slug}/tools`."},"tool_label":{"type":"string","description":"Person-readable phrase for this tool (\"create a contact\"), generated from `tool_name`. Read-only. Show this instead of the raw id. 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.\"), so every client says the same thing. Read-only, derived from `mode`, `connector_slugs` and `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. Show this rather than composing your own."},"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. `null` while `created_by` is a real id means the author has left the org; show 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`, with the same \"Former member\" meaning for a null beside a real id. To decide whether somebody other than the author last wrote the row, compare the two ids, never the two labels: two people can share a display name, and both labels are null once both have left."},"legacy_override":{"type":"boolean","description":"True on a member row that still REPLACES its member's role rules — the meaning every member row had before role and member rules combined, kept on the rows whose conversion could not be proven to leave the member's access unchanged (a role allowlist beside member rows with none). While any of a member's rows carries it, no role rule reaches them, including ones added later. Read-only except that `PATCH` may send `false` to clear it on all of that member's rows, which only ever narrows them."},"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. Always `null` on a `role` target (look the role up by `target_id` in `GET /role`), and `null` on a stale target whose member has been deleted."},"connector_labels":{"type":"object","additionalProperties":{"type":"string"},"description":"Slug to catalog display name for the first 8 connectors the rule enforces (whole connectors first, then tool-only ones), enough for a row's badges and hover text. A slug the catalog could not name (deleted, or the catalog unreachable) is absent: fall back to the slug."},"scope_connectors":{"type":"object","additionalProperties":{"type":"object","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]}}},"description":"One-row responses only (`GET /restriction/{id}`, `POST /restriction`, `PATCH /restriction/{id}`; never a list row): label and logo for every connector the rule scopes (its whole connectors and the connectors its tools belong to), keyed by slug. At most 1000 connectors plus the connectors of 1000 tools. A slug the catalog cannot resolve reads its slug as `label` and a null `logo`."},"can_manage":{"type":"boolean","description":"Whether the caller may create, edit or delete this row: `restriction:manage`, plus two extra checks the write routes 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. A row showing `can_manage: true` will not 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":"Returns one restriction. 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. A member is bound by their role's rules and their own `allow`/`block` rules at once; a connector or tool is reachable only when both admit it. `except` no longer appears here: the way to lift a role rule for one member is an approved access grant (`POST /access-request/{id}/resolve`), not a rule you write or read through this endpoint."},"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 /restriction/connector/{slug}/tools`."},"tool_label":{"type":"string","description":"Person-readable phrase for this tool (\"create a contact\"), generated from `tool_name`. Read-only. Show this instead of the raw id. 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.\"), so every client says the same thing. Read-only, derived from `mode`, `connector_slugs` and `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. Show this rather than composing your own."},"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. `null` while `created_by` is a real id means the author has left the org; show 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`, with the same \"Former member\" meaning for a null beside a real id. To decide whether somebody other than the author last wrote the row, compare the two ids, never the two labels: two people can share a display name, and both labels are null once both have left."},"legacy_override":{"type":"boolean","description":"True on a member row that still REPLACES its member's role rules — the meaning every member row had before role and member rules combined, kept on the rows whose conversion could not be proven to leave the member's access unchanged (a role allowlist beside member rows with none). While any of a member's rows carries it, no role rule reaches them, including ones added later. Read-only except that `PATCH` may send `false` to clear it on all of that member's rows, which only ever narrows them."},"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. Always `null` on a `role` target (look the role up by `target_id` in `GET /role`), and `null` on a stale target whose member has been deleted."},"connector_labels":{"type":"object","additionalProperties":{"type":"string"},"description":"Slug to catalog display name for the first 8 connectors the rule enforces (whole connectors first, then tool-only ones), enough for a row's badges and hover text. A slug the catalog could not name (deleted, or the catalog unreachable) is absent: fall back to the slug."},"scope_connectors":{"type":"object","additionalProperties":{"type":"object","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]}}},"description":"One-row responses only (`GET /restriction/{id}`, `POST /restriction`, `PATCH /restriction/{id}`; never a list row): label and logo for every connector the rule scopes (its whole connectors and the connectors its tools belong to), keyed by slug. At most 1000 connectors plus the connectors of 1000 tools. A slug the catalog cannot resolve reads its slug as `label` and a null `logo`."},"can_manage":{"type":"boolean","description":"Whether the caller may create, edit or delete this row: `restriction:manage`, plus two extra checks the write routes 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. A row showing `can_manage: true` will not 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":"Updates a restriction. 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 /restriction/connector/{slug}/tools`."}}},"description":"Replaces the list."},"legacy_override":{"type":"boolean","enum":[false],"description":"`false` clears a legacy override on every row of that member, so their role rules reach them again beside their own. Only ever narrows them. The flag cannot be set."}}}}}},"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. Filing one grants nothing. Approving a `restriction`-reason request for a connector, or for a tool whose `connector_slug` is set, lifts that restriction for the requester alone; every other approval is a record for a human to act on through the role and 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":"What was asked for: a tool name, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"]},"connector_slug":{"type":["string","null"],"description":"On a `\"tool\"` request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a `\"connector\"` request."},"connector_label":{"type":"string","description":"Display name of `connector_slug` (or, on a `\"connector\"` request, of `tool` itself), falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue."},"connector_logo":{"type":["string","null"],"description":"The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`."},"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"]},"restriction_lifted":{"type":["boolean","null"],"description":"What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: \"policy\"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."},"will_lift_restriction":{"type":"boolean","description":"On a pending restriction-reason request approving could act on (a connector request, or a tool request naming its `connector_slug`): whether approving it would actually lift anything. Absent on every other row."},"connector_restricted_for_requester":{"type":"boolean","description":"On the same rows, for a TOOL request only: true when approving lifts nothing because the requester’s WHOLE connector is restricted — one tool cannot be carved out of a connector restricted whole, and an approval never opens the whole connector for a one-tool ask. Their connector access is what an admin would have to grant instead."},"lifts_inherited_restriction":{"type":"boolean","description":"On the same rows, for a CONNECTOR or TOOL request: true when approving opens this FORK (or, for a tool request, that one tool on it) by lifting a block it inherits from the connector it was forked from — through the requester’s role OR through a rule written about them personally. A request for a fork is its own ask: approving it opens THAT FORK for THAT PERSON and nothing else; the original stays restricted for them, a rule naming the fork itself is not overridden, a connector approval never lifts their personal tool rules, and the exception is standing and tied to the fork (revoking the grant re-blocks it)."},"lifts_inherited_member_rule":{"type":"boolean","description":"With `lifts_inherited_restriction`: true when the block it lifts is a rule written about the requester personally rather than their role’s. Only then does the grant carry the marker that keeps lifting it while the grant is active — for the originals that blocked it when it was approved, so a block written later on a different ancestor is not bypassed. A lift of a role-layer block alone does not carry it, so a personal block written later on the original is not overridden by it."},"inherited_from_member_rule":{"type":"boolean","description":"On the same rows, for a CONNECTOR request only: true when approving lifts nothing even though the connector is a fork blocked through its original in the requester’s own restrictions. This is now rare: most such forks are covered by `lifts_inherited_restriction` instead."},"inherited_from_connector_label":{"type":["string","null"],"description":"With `lifts_inherited_restriction` or `inherited_from_member_rule`: the display name of the connector it was forked from, or `null` when it cannot be named to this caller (private to someone else, deleted, or the catalog did not answer). Never a slug or id."}}}]},{"allOf":[{"type":"object","description":"A member’s recourse when a tool call is refused for a missing permission or an org restriction. Filing one grants nothing. Approving a `restriction`-reason request for a connector, or for a tool whose `connector_slug` is set, lifts that restriction for the requester alone; every other approval is a record for a human to act on through the role and 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":"What was asked for: a tool name, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"]},"connector_slug":{"type":["string","null"],"description":"On a `\"tool\"` request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a `\"connector\"` request."},"connector_label":{"type":"string","description":"Display name of `connector_slug` (or, on a `\"connector\"` request, of `tool` itself), falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue."},"connector_logo":{"type":["string","null"],"description":"The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`."},"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"]},"restriction_lifted":{"type":["boolean","null"],"description":"What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: \"policy\"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it."},"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, because resolving it into a name and email is a member-directory lookup this view does not hand a plain member.","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":"Lists access requests, for two audiences on one route. **Without `mine`** it is the org-wide admin queue and requires `member:manage`; a caller without it gets `403`, never an empty page. Rows are the admin view (`requester`, `resolved_by`, `can_resolve`).\n\n**With `mine=true`** it returns only the caller’s own requests and needs no permission, so the person who filed a request can find it again. 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.\n\n`limit`, `cursor`, `q` and `status` apply to both. `q` matches the requested tool name or the requester’s note, not the requester themselves; to find requests by person, list members and filter on `requester_user_id`. It does not match `tool_label`.","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. Filing one grants nothing. Approving a `restriction`-reason request for a connector, or for a tool whose `connector_slug` is set, lifts that restriction for the requester alone; every other approval is a record for a human to act on through the role and 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":"What was asked for: a tool name, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"]},"connector_slug":{"type":["string","null"],"description":"On a `\"tool\"` request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a `\"connector\"` request."},"connector_label":{"type":"string","description":"Display name of `connector_slug` (or, on a `\"connector\"` request, of `tool` itself), falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue."},"connector_logo":{"type":["string","null"],"description":"The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`."},"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"]},"restriction_lifted":{"type":["boolean","null"],"description":"What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: \"policy\"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it."},"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, because resolving it into a name and email is a member-directory lookup this view does not hand a plain member.","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. Filing one grants nothing. Approving a `restriction`-reason request for a connector, or for a tool whose `connector_slug` is set, lifts that restriction for the requester alone; every other approval is a record for a human to act on through the role and 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":"What was asked for: a tool name, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"]},"connector_slug":{"type":["string","null"],"description":"On a `\"tool\"` request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a `\"connector\"` request."},"connector_label":{"type":"string","description":"Display name of `connector_slug` (or, on a `\"connector\"` request, of `tool` itself), falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue."},"connector_logo":{"type":["string","null"],"description":"The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`."},"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"]},"restriction_lifted":{"type":["boolean","null"],"description":"What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: \"policy\"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it."},"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, because resolving it into a name and email is a member-directory lookup this view does not hand a plain member.","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":"Files a request for access. Any member may call it, with no permission required, because asking for a permission must not itself need one. A request is always filed as the caller, so the body carries no requester. `permission` is required and must be a known permission name when `reason` is `\"permission\"`, and forbidden (`400`) when `reason` is `\"restriction\"`: a restriction refusal never names the rule that blocked someone, and this resource must not become a second way to learn it. `reason` is never `\"scope\"`, because a scope refusal has no admin-side fix.\n\n**Filing never grants anything.** It creates a record for an admin to decide; approving a `restriction`-reason request is what can lift the restriction (see `POST /access-request/{id}/resolve`).\n\n**De-duplicated.** A second request for the same `tool` while an earlier one from the same requester is still `pending` returns the existing row (updating `note` when a different one was sent) with `200`. A genuinely new row is `201`.\n\n**A tool request that could never be granted is refused.** A `restriction`-reason `\"tool\"` request naming a `connector_slug` whose whole connector is restricted for the caller is `409 tool_request_connector_blocked`: approving it would lift nothing, because one tool cannot be carved out of a connector restricted whole. The message names the connector and says to ask for it instead; `error.details` carries `connector_slug`, `connector_label` and `pending_request` (the caller’s own open connector request, or null). Every tool row that can draw an ask carries `restricted_scope` (`\"connector\"`, `\"tool\"` or null), so a client can file the connector request up front.","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, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"],"default":"tool"},"connector_slug":{"type":"string","maxLength":200,"description":"Only on a `\"tool\"` request (400 on a connector request): the connector the tool lives on. Lets an approval lift the restriction on exactly that tool; without it approving records a decision only."},"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. Filing one grants nothing. Approving a `restriction`-reason request for a connector, or for a tool whose `connector_slug` is set, lifts that restriction for the requester alone; every other approval is a record for a human to act on through the role and 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":"What was asked for: a tool name, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"]},"connector_slug":{"type":["string","null"],"description":"On a `\"tool\"` request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a `\"connector\"` request."},"connector_label":{"type":"string","description":"Display name of `connector_slug` (or, on a `\"connector\"` request, of `tool` itself), falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue."},"connector_logo":{"type":["string","null"],"description":"The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`."},"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"]},"restriction_lifted":{"type":["boolean","null"],"description":"What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: \"policy\"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it."},"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, because resolving it into a name and email is a member-directory lookup this view does not hand a plain member.","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. Filing one grants nothing. Approving a `restriction`-reason request for a connector, or for a tool whose `connector_slug` is set, lifts that restriction for the requester alone; every other approval is a record for a human to act on through the role and 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":"What was asked for: a tool name, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"]},"connector_slug":{"type":["string","null"],"description":"On a `\"tool\"` request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a `\"connector\"` request."},"connector_label":{"type":"string","description":"Display name of `connector_slug` (or, on a `\"connector\"` request, of `tool` itself), falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue."},"connector_logo":{"type":["string","null"],"description":"The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`."},"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"]},"restriction_lifted":{"type":["boolean","null"],"description":"What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: \"policy\"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."},"will_lift_restriction":{"type":"boolean","description":"On a pending restriction-reason request approving could act on (a connector request, or a tool request naming its `connector_slug`): whether approving it would actually lift anything. Absent on every other row."},"connector_restricted_for_requester":{"type":"boolean","description":"On the same rows, for a TOOL request only: true when approving lifts nothing because the requester’s WHOLE connector is restricted — one tool cannot be carved out of a connector restricted whole, and an approval never opens the whole connector for a one-tool ask. Their connector access is what an admin would have to grant instead."},"lifts_inherited_restriction":{"type":"boolean","description":"On the same rows, for a CONNECTOR or TOOL request: true when approving opens this FORK (or, for a tool request, that one tool on it) by lifting a block it inherits from the connector it was forked from — through the requester’s role OR through a rule written about them personally. A request for a fork is its own ask: approving it opens THAT FORK for THAT PERSON and nothing else; the original stays restricted for them, a rule naming the fork itself is not overridden, a connector approval never lifts their personal tool rules, and the exception is standing and tied to the fork (revoking the grant re-blocks it)."},"lifts_inherited_member_rule":{"type":"boolean","description":"With `lifts_inherited_restriction`: true when the block it lifts is a rule written about the requester personally rather than their role’s. Only then does the grant carry the marker that keeps lifting it while the grant is active — for the originals that blocked it when it was approved, so a block written later on a different ancestor is not bypassed. A lift of a role-layer block alone does not carry it, so a personal block written later on the original is not overridden by it."},"inherited_from_member_rule":{"type":"boolean","description":"On the same rows, for a CONNECTOR request only: true when approving lifts nothing even though the connector is a fork blocked through its original in the requester’s own restrictions. This is now rare: most such forks are covered by `lifts_inherited_restriction` instead."},"inherited_from_connector_label":{"type":["string","null"],"description":"With `lifts_inherited_restriction` or `inherited_from_member_rule`: the display name of the connector it was forked from, or `null` when it cannot be named to this caller (private to someone else, deleted, or the catalog did not answer). Never a slug or id."}}}]}],"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":"Returns one access request. What the caller sees depends on who they are: an admin (`member:manage`) sees any request as the admin view, the request’s own requester sees it as the self view, and anyone else gets `404`, because 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":"Approve or deny an 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. Filing one grants nothing. Approving a `restriction`-reason request for a connector, or for a tool whose `connector_slug` is set, lifts that restriction for the requester alone; every other approval is a record for a human to act on through the role and 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":"What was asked for: a tool name, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"]},"connector_slug":{"type":["string","null"],"description":"On a `\"tool\"` request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a `\"connector\"` request."},"connector_label":{"type":"string","description":"Display name of `connector_slug` (or, on a `\"connector\"` request, of `tool` itself), falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue."},"connector_logo":{"type":["string","null"],"description":"The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`."},"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"]},"restriction_lifted":{"type":["boolean","null"],"description":"What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: \"policy\"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it."},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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"},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."}}},"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."},"will_lift_restriction":{"type":"boolean","description":"On a pending restriction-reason request approving could act on (a connector request, or a tool request naming its `connector_slug`): whether approving it would actually lift anything. Absent on every other row."},"connector_restricted_for_requester":{"type":"boolean","description":"On the same rows, for a TOOL request only: true when approving lifts nothing because the requester’s WHOLE connector is restricted — one tool cannot be carved out of a connector restricted whole, and an approval never opens the whole connector for a one-tool ask. Their connector access is what an admin would have to grant instead."},"lifts_inherited_restriction":{"type":"boolean","description":"On the same rows, for a CONNECTOR or TOOL request: true when approving opens this FORK (or, for a tool request, that one tool on it) by lifting a block it inherits from the connector it was forked from — through the requester’s role OR through a rule written about them personally. A request for a fork is its own ask: approving it opens THAT FORK for THAT PERSON and nothing else; the original stays restricted for them, a rule naming the fork itself is not overridden, a connector approval never lifts their personal tool rules, and the exception is standing and tied to the fork (revoking the grant re-blocks it)."},"lifts_inherited_member_rule":{"type":"boolean","description":"With `lifts_inherited_restriction`: true when the block it lifts is a rule written about the requester personally rather than their role’s. Only then does the grant carry the marker that keeps lifting it while the grant is active — for the originals that blocked it when it was approved, so a block written later on a different ancestor is not bypassed. A lift of a role-layer block alone does not carry it, so a personal block written later on the original is not overridden by it."},"inherited_from_member_rule":{"type":"boolean","description":"On the same rows, for a CONNECTOR request only: true when approving lifts nothing even though the connector is a fork blocked through its original in the requester’s own restrictions. This is now rare: most such forks are covered by `lifts_inherited_restriction` instead."},"inherited_from_connector_label":{"type":["string","null"],"description":"With `lifts_inherited_restriction` or `inherited_from_member_rule`: the display name of the connector it was forked from, or `null` when it cannot be named to this caller (private to someone else, deleted, or the catalog did not answer). Never a slug or id."}}}]}}}},"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":"Approves or denies someone else’s access request. Requires `member:manage`. Only an interactive browser session can decide: an organization API token (`Bearer elch_…`) is refused with `403`, however many permissions it holds, because approving an access request is a person’s decision. The session must also pass step-up reauthentication in the app; without a valid `X-Step-Up-Token` the call answers `428 step_up_required`, and `error.details` names the action and resource to prove. A decision is recorded once and never re-opened, so the person recording it re-proves they are still the person holding the session. Refuses with `409` when the request has already left `pending`.\n\n**An admin may not decide their own request: `403`**, whichever decision they send and however many permissions they hold. Self-approval of a restriction request would be a self-grant, and the record would read as approved by a second person when it was not. The requesting admin keeps `can_withdraw` on that row instead.\n\n**Approving a `restriction`-reason request with `resolution: \"grant\"` (the default) lifts that restriction for the requester alone**, either a whole connector or one tool on the connector a tool request names in `connector_slug`, by writing an access grant (who, what, approved by, when, and optionally an expiry). The requester’s own member-level rules are edited (and a block rule the approval empties is removed); where their role’s rules stand in the way, the grant is what lifts them. Role rules themselves are never edited or copied, so every other role rule, including one added later, keeps binding the requester. A tool on a connector blocked whole cannot be carved out and is not lifted. `resolution: \"policy\"` instead records the decision without writing a grant, for when the admin is fixing the underlying role or connector rule directly through the restriction APIs. If the requester has a personal rule that would need editing to open the target, an approval that lists `remove_member_rules` (the ids of the personal rules the caller confirms editing) must list exactly the conflicting ids, or the call answers `409 member_rule_conflict` naming them in `error.details.member_rule_conflicts`. Omitting `remove_member_rules` skips that check and edits the personal rules without confirmation, for clients that predate the field. `expires_at` (ISO, in the future, at most about 10 years out; any UTC offset is accepted and stored as UTC) sets when a grant expires; omit it or send `null` for no expiry. `will_lift_restriction` on the admin view previews whether approving will change anything. For a fork blocked through its original (in the requester’s role or in their own rules), approving opens that fork for that person only and `lifts_inherited_restriction` says so: the original stays restricted for them, the grant is audited with `lifted_inherited_restriction: true`, and revoking it re-blocks the fork. Every other approval (a `permission`-reason request, or a tool request with no `connector_slug`) records a decision only; widening access there is a separate act through `PATCH /member/{userId}` or the restriction APIs.","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},"resolution":{"type":"string","enum":["grant","policy"],"description":"Only meaningful on an APPROVED, `reason: \"restriction\"` request. `grant` (default) writes an access grant; `policy` approves without writing one — the admin is changing the underlying rule directly instead."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the grant this approval writes auto-expires. `null`/omitted means no expiry. Ignored unless this approval actually writes a grant."},"remove_member_rules":{"type":"array","items":{"type":"string"},"description":"The ids of the requester’s personal rules that this approval would edit, as the caller confirms them. Must equal the conflicting set exactly (an empty array asserts there are none), or the call answers `409 member_rule_conflict` with the real ids in `error.details`. Omit the field to skip the check; the rules are then edited without confirmation."}}}}}},"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. Filing one grants nothing. Approving a `restriction`-reason request for a connector, or for a tool whose `connector_slug` is set, lifts that restriction for the requester alone; every other approval is a record for a human to act on through the role and 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":"What was asked for: a tool name, or a connector slug when `resource_type` is `\"connector\"`."},"resource_type":{"type":"string","enum":["tool","connector"]},"connector_slug":{"type":["string","null"],"description":"On a `\"tool\"` request, the connector the tool lives on when the request named one — what makes approving it able to lift the restriction on that tool. Always null on a `\"connector\"` request."},"connector_label":{"type":"string","description":"Display name of `connector_slug` (or, on a `\"connector\"` request, of `tool` itself), falling back to the slug. Present on every view: the requester reads it in the self view exactly as an admin does in the queue."},"connector_logo":{"type":["string","null"],"description":"The connector’s logo for the slug `connector_label` names. `null` when the catalog has no logo for the slug; present under the same conditions as `connector_label`."},"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"]},"restriction_lifted":{"type":["boolean","null"],"description":"What an approval did to the requester’s access, on every view: `true` when it wrote a grant, `false` when the request was approved and nothing was lifted (a `resolution: \"policy\"` approval, access that was already open, or a shape no grant can open), `null` when the question does not apply (not approved, a `permission` request, a tool request with no `connector_slug`). It never says why; the connector’s own `restricted`, `restricted_by` and `restricted_inherited_from` do, within what the caller may be told. An older approval may read `false` for access it did give, so read the connector’s `restricted` for the live answer. It is the outcome at resolve time: a later revoke, expiry or replacement does not change it."},"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, because resolving it into a name and email is a member-directory lookup this view does not hand a plain member.","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":"Withdraws an access request. Only the request’s own requester may withdraw it, and only while it is still `pending`. No body, and no permission beyond membership. 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":"Lists the organization’s logging destinations (Datadog, Splunk or Sentinel), cursor-paginated. 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":"Creates a logging destination that receives a copy of the audit trail. 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":"Returns one logging destination. 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":"Updates a logging destination. `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":"Deletes a logging destination. 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":"Sends a test event to a logging destination. 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":"Lists the organization's notification destinations (Slack or email), cursor-paginated. 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":"Creates a notification destination (Slack or email). 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":"Returns one notification destination. 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":"Updates a notification destination. `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":"Deletes a notification destination. 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":"Sends a test event to a notification destination. 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":"Lists the organization's SSO connections (SAML and OIDC), cursor-paginated. 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":"Creates an SSO connection. 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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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":"Returns one SSO connection, including the `urls` to hand to the IdP administrator. Secrets are never included. 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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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":"Deletes an SSO connection. 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-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":"Lists the members of a SCIM group. 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":"Lists the SCIM group to role mappings, 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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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":"Returns one SCIM group to role mapping. 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":"Updates a group mapping. **`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 also pass a fresh step-up reauthentication in the app (without it the call answers `428 step_up_required`). An organization API token is not challenged.","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":"Get 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":"List 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":"List 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":"List users (SCIM)","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":"Lists the users provisioned into the token’s organization. The SCIM endpoints are authenticated by a SCIM bearer token (created in the app under Settings → SSO and SCIM), not a session or an org API token; the organization comes from the token, so send no `X-Organization-Id`. They paginate with SCIM’s `startIndex` and `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":"Create a user (SCIM)","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":"Get a user (SCIM)","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":"Returns one provisioned user by SCIM id.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM user id."}]},"put":{"operationId":"scimReplaceUser","summary":"Replace a user (SCIM)","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":"Update a user (SCIM)","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":"Delete a user (SCIM)","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":"List groups (SCIM)","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":"Lists the groups the IdP has pushed. Paginates with SCIM’s `startIndex` and `count`.","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":"Create a group (SCIM)","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":"Get a group (SCIM)","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":"Returns one group by SCIM id, with its members.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"SCIM group id."}]},"put":{"operationId":"scimReplaceGroup","summary":"Replace a group (SCIM)","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":"Update a group (SCIM)","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":"Delete a group (SCIM)","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."}]}},"/file":{"get":{"operationId":"listFiles","summary":"List tool files you can reach","tags":["Files"],"responses":{"200":{"description":"OK. Beside the usual envelope: `can_upload` (whether uploads are available) and `upload_limits` (the limits every upload shows), repeated on every page.","content":{"application/json":{"schema":{"type":"object","required":["result","next_cursor","prev_cursor","can_upload","upload_limits"],"properties":{"result":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"File id (`file_…`)."},"organization_id":{"type":"string"},"created_by_user_id":{"type":"string","description":"The member who ran the tool: the file’s owner. Not the owner of the connection behind it."},"filename":{"type":"string"},"mime":{"type":"string"},"size_bytes":{"type":"integer"},"connection_id":{"type":["string","null"],"description":"Provenance, and one source of access: a member with `use` or better on this connection sees the file in `GET /file` and can open it. It is never the owner’s gate: a `use` grantee can own a file made over a connection they cannot see at all. **Present only on rows the caller owns** — omitted, not null, on a file shared with them."},"toolbox_id":{"type":["string","null"],"description":"Provenance, and a second source of access: `use` or better on this toolbox lists and opens the file (while the toolbox still pins the file’s connection). **Present only on rows the caller owns** — omitted, not null, on a file shared with them."},"tool_name":{"type":["string","null"],"description":"The model-facing tool name; can be an opaque `tb__…` string. Never show it to a person — word `tool_resource` and `tool_method` instead. **Present only on rows the caller owns** — omitted, not null, on a file shared with them."},"tool_resource":{"type":["string","null"],"description":"Catalog resource of the tool that produced the file (e.g. `slides`), safe to show a person. Set on every file, because only a connected tool can produce one."},"tool_method":{"type":["string","null"],"description":"Catalog method of the tool that produced the file (e.g. `export`), safe to show a person. Set on every file."},"delegated_by_user_id":{"type":["string","null"],"description":"Set when the tool ran over a connection delegated to the creator; null when they used their own. **Present only on rows the caller owns** — omitted, not null, on a file shared with them."},"created_at":{"type":"string","format":"date-time"},"origin":{"type":"string","enum":["tool","upload","request","inline","api","url_import","web_fetch","export"],"description":"How the bytes got here: `tool` (a connector tool returned them), `upload` (a person chose it on the Files screen), `request` (a person handed it to an app that asked, through an upload request; `origin_host` is the app), `inline` (content an assistant wrote with `file.create`), `api` (a backend sent it to `POST /file` with a token), `url_import`, `web_fetch` (fetched from the web), `export` (produced by an export). Provenance for wording (“Uploaded by Dana”), never an access gate."},"origin_host":{"type":["string","null"],"description":"The web host an import or web fetch read from, or the asking app’s name for origin `request`; null otherwise."},"expires_at":{"type":"string","format":"date-time","description":"Defaults to 7 days after creation. Enforced when the file is read: past this instant the file is absent from `GET /file` and answers `404` on the content route, for the owner and every grantee alike. The stored file is deleted shortly afterwards."},"access":{"type":"string","enum":["owner","view"],"description":"The caller’s own reach: `owner` for a file they ran the tool for, `view` for one an owner shared with them or one they reach through its source connection or toolbox."},"access_via":{"type":"string","enum":["owner","direct","team","org","connection","toolbox"],"description":"How the caller reaches this file, not who else can. `owner`: they own it. `direct`: a grant names them personally. `team`: a grant to a team they belong to (or administer), named in `access_via_team`. `org`: an organization-wide grant. When several sources apply, the broadest wins and the access level plays no part: owner, else `org`, else `team`, else `direct`. A caller with `edit` personally and `view` org-wide reads `org`, because this field says how far the file reaches, not what the caller may do with it. It describes only the caller's own access, so a `view` or `use` grantee still gets it while the list of other grantees stays hidden. Absent when nothing reaches the caller (for example, the former owner after a transfer of a resource that was never shared); absent never means `org`.\n\nA file adds two values for access nobody granted by name: `connection` (the caller has `use` or better on the connection the file came through) and `toolbox` (they have `use` or better on the toolbox it ran in, and that toolbox still pins the file’s connection). A named grant (`direct`, `team` or `org`) outranks both when it also applies, and `toolbox` outranks `connection` when a file is reached both ways. Only file rows carry `connection` and `toolbox`."},"access_via_team":{"type":"object","description":"Present exactly when `access_via` is `team`, absent otherwise. The team the caller reaches this file 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."}}},"created_by":{"type":"object","description":"Who ran the tool. `name` is null for someone who has left the organization.","properties":{"id":{"type":"string","description":"User id (`usr_…`) — same value as `created_by_user_id`."},"name":{"type":["string","null"]}}},"can_delete":{"type":"boolean","description":"Whether the caller may `DELETE /file/{id}` — the owner, and only the owner. No permission verb to mirror."},"can_share":{"type":"boolean","description":"Whether the caller may `POST /file/{id}/share` — the owner, and only the owner. There is no `file:share` permission; ownership is the gate."},"can_see_shares":{"type":"boolean","description":"Whether the caller may `GET /file/{id}/share` — the owner, and only the owner. Read this rather than inferring \"not permitted\" from the absence of `access_summary`."},"can_revoke_share":{"type":"boolean","description":"Whether the caller may `DELETE /file/{id}/share/{aclId}` — the owner, and only the owner. Named for the question it answers: no `file:revoke` verb exists."},"can_open":{"type":"boolean","description":"Whether the file’s bytes will be served to the caller right now. `true` for an owned or explicitly shared row. On a row reached through its source connection or toolbox it is `false` when an org restriction blocks the connector or the tool (`restricted_by` is then set), when the toolbox’s delegation no longer holds (its pinning entry’s delegator lost `use`), or when the source connection has been deleted; in the last two cases `restricted_by` is null. If the restriction lookup itself fails, such rows read `false` too. This is a prediction for the page, not a second gate: `GET /file/{id}/content` stays the authority and answers a uniform `404` for anything this says `false` to."},"restricted_by":{"type":["string","null"],"enum":["role","user",null],"description":"Which precedence layer’s restriction blocks this file for the caller, null when none does. Same field and meaning as `restricted_by` on a connector or connection row: it adds which layer to `can_open`’s whether, and no rule id, author or reason. Non-null only on rows reached through a source connection or toolbox (`access_via` is `connection` or `toolbox`), because a restriction applies to that route alone: an owned or explicitly shared row is always null, and so is a row that will not open for any other reason (a lapsed toolbox delegation, a deleted connection). The row is listed and flagged, never omitted, so a page can hold rows the caller cannot open."},"access_summary":{"type":"object","description":"The file’s audience as a short summary. Present on rows the caller owns only, never on a file shared with them (the summary describes the owner’s audience, which a grantee has no business seeing). Grants are `view` only. Page `GET /file/{id}/share` for the grants themselves.","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."}}}}}},"kind":{"type":"string","enum":["document","spreadsheet","presentation","image","archive","other"],"description":"Coarse bucket derived from the stored `mime` by the same classification `?type=` filters with. `other` is everything the named kinds do not claim."},"connector_slug":{"type":["string","null"],"description":"Connector of the connection the file came through (the `?connector_slug=` filter). Null when it came through no connection or that connection has since been deleted."},"connector":{"type":["object","null"],"description":"`connector_slug`’s display data, the same as on a `GET /connection` row. Null exactly when `connector_slug` is; a connector the catalog cannot resolve reads its slug as `label` and a null `logo`.","required":["label","logo"],"properties":{"label":{"type":"string"},"logo":{"type":["string","null"]},"upstream_slug":{"type":"string","description":"Only on a fork whose upstream the caller may see, by the same rule as `GET /connector`."}}}}}},"next_cursor":{"type":["string","null"]},"prev_cursor":{"type":["string","null"]},"can_upload":{"type":"boolean"},"upload_limits":{"type":"object","properties":{"max_files":{"type":"integer","description":"Files at most. 10."},"max_file_bytes":{"type":"integer","description":"The most one file may be. 95 MiB, or the request’s lower setting."},"max_total_bytes":{"type":"integer","description":"The most the whole request may hold. 250 MiB."},"accept":{"type":["array","null"],"items":{"type":"string"},"description":"Allowed media types (`image/*`, `application/pdf`) and extensions (`.csv`); null means anything."}}}}}}}},"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":"Lists every file the caller can reach, newest first, never an expired one: the files they own, the files an owner shared with them by name (directly, through a team they are in, or organization-wide), and the files they reach through the source: `use` or better on the connection the file came through, or on the toolbox it ran in (the toolbox only while it still pins the file’s connection with a working delegation). A file is treated like the connection or toolbox it came out of: if you can use what made it, you see it. No permission is required beyond organization membership, and there is no admin view: an org owner sees what they own or reach, like anyone else.\n\n**A file your organization’s restrictions block is still listed**, flagged `restricted_by` with `can_open: false`, the way a blocked connection stays in `GET /connection`. `can_open` is also `false` on a row reached through a toolbox whose delegation no longer holds or whose source connection has been deleted, and on every such row when the restriction lookup failed. The content route stays the authority.\n\n**Cost.** An unfiltered page does work proportional to the number of connections and toolboxes the caller can use times the page size, not to the number of files. A `connector_slug` filter narrows that and stays cheap; a rare substring `q` and `created_before` can still scan much of what the caller reaches before a page fills (bounded by the 7-day retention).\n\nA caller that already ran the tool has the id from the tool result and can skip this entirely. `?q=`, `?scope=`, `?connection_id=`, `?connector_slug=`, `?type=` and `?created_before=` are all applied before paging. Every row says how the caller reaches it (`access`, and `access_via`, which for a file may also be `connection` or `toolbox`), whether it will open (`can_open`, `restricted_by`) and what they may do to it (`can_delete`, `can_share`, `can_see_shares`, `can_revoke_share`, all owner-only). A row the caller does not own omits the owner’s provenance (`connection_id`, `toolbox_id`, `tool_name` and `delegated_by_user_id`) and `access_summary`; it keeps the file, `connector_slug`, `tool_resource` and `tool_method` (the words for “from …”), `created_by` and the caller’s own `access_via`.","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 file’s filename. LIKE wildcards are matched literally. Max 200 characters.","schema":{"type":"string","maxLength":200}},{"name":"scope","in":"query","required":false,"description":"`owned` — only files the caller ran the tool for. `shared` — every visible file the caller did NOT make: explicitly shared with them AND reached through a source connection or toolbox. Absent — both.","schema":{"type":"string","enum":["owned","shared"]}},{"name":"connection_id","in":"query","required":false,"description":"Only the caller’s OWN files produced through this connection. A row the caller does not own does not disclose its `connection_id`, so this filter never matches one.","schema":{"type":"string"}},{"name":"connector_slug","in":"query","required":false,"description":"Only files produced through a connection of one of these connectors. Comma-separated or repeated, at most 25 (more is a `400`). Files with no surviving connection never match.","schema":{"type":"string"}},{"name":"type","in":"query","required":false,"description":"Only files of these kinds (the row’s `kind`). Comma-separated, any of `document`, `spreadsheet`, `presentation`, `image`, `archive`, `other` (anything else is a `400`).","schema":{"type":"string"}},{"name":"created_before","in":"query","required":false,"description":"Only files created strictly before this ISO-8601 timestamp.","schema":{"type":"string","format":"date-time"}}]},"post":{"operationId":"createFile","summary":"Upload a file","tags":["Files"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"object","properties":{"file_id":{"type":"string"},"status":{"type":"string","enum":["pending","ready","expired"],"description":"`pending`: an upload link you made is waiting for its bytes (not a file yet: no type, size or URL). `ready`: a file you can open. `expired`: your link ran out unused, or the file it filled is gone."},"filename":{"type":"string"},"mime":{"type":["string","null"]},"size_bytes":{"type":["integer","null"]},"expires_at":{"type":"string","format":"date-time"},"url":{"type":["string","null"],"description":"The Elaichi app’s download page, for a `ready` file."},"origin":{"type":"string","enum":["tool","upload","request","inline","api","url_import","web_fetch","export"],"description":"How the bytes got here: `tool` (a connector tool returned them), `upload` (a person chose it on the Files screen), `request` (a person handed it to an app that asked, through an upload request; `origin_host` is the app), `inline` (content an assistant wrote with `file.create`), `api` (a backend sent it to `POST /file` with a token), `url_import`, `web_fetch` (fetched from the web), `export` (produced by an export). Provenance for wording (“Uploaded by Dana”), never an access gate."},"origin_host":{"type":["string","null"],"description":"The web host an import or web fetch read from, or the asking app’s name for origin `request`; null otherwise."},"max_bytes":{"type":["integer","null"],"description":"The link’s size limit while `pending`."},"access":{"type":["string","null"],"enum":["owner","view",null]},"can_open":{"type":"boolean"},"can_delete":{"type":"boolean"},"can_share":{"type":"boolean"},"can_see_shares":{"type":"boolean"},"can_revoke_share":{"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":"Uploads a file in one request, for a backend that holds a session or an API token, and for the Elaichi app’s Files screen (which uses it for **Upload**). Two forms, told apart by `X-Filename` (or `?filename=`):\n\n- **With it, the body is the file** (`Content-Type` is the declared type, `Content-Length` is required, at most 95 MiB). Streamed straight into storage with the size counted on the bytes actually read, so a header that lies is caught mid-stream and nothing is kept. `411` `upload_length_required` without a length, `413` `upload_too_large` over the limit. A raw upload of a JSON file sends `X-Filename`, so it is never taken for the second form.\n- **Without it, the body is JSON** `{ filename, content, encoding: utf8 | base64, mime? }` for content the client wrote itself, at most 1 MiB of decoded content (`413` `inline_content_too_large`; the body is never buffered past that, whatever it declares).\n\nEither way the answer is `201 { file }`, the file as `GET /file/{id}` reads it, owned by the caller and kept for 7 days. The stored type is read from the bytes, never taken from the declaration. `origin` is `upload` for a person’s session, `api` for a token. Audited as `file.uploaded`.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["filename","content"],"properties":{"filename":{"type":"string","maxLength":500},"content":{"type":"string"},"encoding":{"type":"string","enum":["utf8","base64"],"default":"utf8"},"mime":{"type":"string","maxLength":255}}}},"*/*":{"schema":{"type":"string","format":"binary"}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"X-Filename","in":"header","required":false,"description":"The file’s name; may be percent-encoded. Selects the raw-body form.","schema":{"type":"string"}},{"name":"filename","in":"query","required":false,"description":"Same as `X-Filename`, for a client that cannot set headers.","schema":{"type":"string"}}]}},"/file/upload-request":{"post":{"operationId":"createFileUploadRequest","summary":"Request files from a person","tags":["Files"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","description":"Request id (`fupr_…`)."},"status":{"type":"string","enum":["open","confirmed","expired"],"description":"`open`: waiting for the person (files may be added, removed, confirmed). `confirmed`: they approved; the staged files are now theirs. `expired`: the deadline passed unconfirmed with nothing still uploading. A request whose upload STARTED before the deadline stays open until that upload finishes, and each finished file keeps it open for at least five more minutes, so someone mid-upload is never told it expired."},"purpose":{"type":"string","description":"What the files are for, as the asker said it."},"requester":{"type":"string","description":"Who is asking: the connected app’s name, or “Elaichi assistant”."},"limits":{"type":"object","properties":{"max_files":{"type":"integer","description":"Files at most. 10."},"max_file_bytes":{"type":"integer","description":"The most one file may be. 95 MiB, or the request’s lower setting."},"max_total_bytes":{"type":"integer","description":"The most the whole request may hold. 250 MiB."},"accept":{"type":["array","null"],"items":{"type":"string"},"description":"Allowed media types (`image/*`, `application/pdf`) and extensions (`.csv`); null means anything."}}},"created_at":{"type":"string","format":"date-time"},"add_files_until":{"type":"string","format":"date-time","description":"Ten minutes after creation, fixed. No new file can be reserved after it; the person can still send the files they added."},"expires_at":{"type":"string","format":"date-time","description":"The last moment the person can confirm (Send). Starts at `add_files_until`; it moves out when an upload finishes or an upload that was running ends without landing, never past 35 minutes after creation."},"upload_page_url":{"type":"string","description":"The Elaichi app page where the person confirms. Carries no secret: only the signed-in person who made the request can use it."},"files":{"type":"array","description":"At most `limits.max_files`.","items":{"type":"object","properties":{"file_id":{"type":"string"},"filename":{"type":"string"},"size_bytes":{"type":["integer","null"]},"mime":{"type":["string","null"]},"state":{"type":"string","enum":["uploading","staged","ready","stalled"]}}}}}}}}},"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":"Makes an upload request: up to 10 files (`max_files`), 95 MiB each (`max_file_bytes`), 250 MiB in all, optionally restricted to `accept` types. It lapses 10 minutes after it is made unless the person confirms. There is no link to hand around and no secret: the person opens `upload_page_url` in the Elaichi app, must be signed in as the account that made the request, chooses the files (each uploads in the background and can be removed again) and presses **Confirm**, which is their approval to hand the files over. Only confirmed files exist as files; until then a tool naming one is refused `file_pending`. Bounded per person: at most 20 requests open at once (`429` `too_many_open_uploads`) and 100 made per hour (`429` `too_many_upload_requests`, counted on requests made, so deleting files does not reset it). Audited as `file.upload_requested`. A backend that needs a file of its own, with no person involved, uses `POST /file` instead.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["purpose"],"properties":{"purpose":{"type":"string","maxLength":500,"description":"A short sentence the person reads on the page."},"max_files":{"type":"integer","minimum":1,"maximum":10},"accept":{"type":"array","items":{"type":"string"},"maxItems":40},"max_file_bytes":{"type":"integer","minimum":1,"maximum":99614720}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"}]}},"/file/upload-request/{id}":{"get":{"operationId":"getFileUploadRequest","summary":"Read an upload request","tags":["Files"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","description":"Request id (`fupr_…`)."},"status":{"type":"string","enum":["open","confirmed","expired"],"description":"`open`: waiting for the person (files may be added, removed, confirmed). `confirmed`: they approved; the staged files are now theirs. `expired`: the deadline passed unconfirmed with nothing still uploading. A request whose upload STARTED before the deadline stays open until that upload finishes, and each finished file keeps it open for at least five more minutes, so someone mid-upload is never told it expired."},"purpose":{"type":"string","description":"What the files are for, as the asker said it."},"requester":{"type":"string","description":"Who is asking: the connected app’s name, or “Elaichi assistant”."},"limits":{"type":"object","properties":{"max_files":{"type":"integer","description":"Files at most. 10."},"max_file_bytes":{"type":"integer","description":"The most one file may be. 95 MiB, or the request’s lower setting."},"max_total_bytes":{"type":"integer","description":"The most the whole request may hold. 250 MiB."},"accept":{"type":["array","null"],"items":{"type":"string"},"description":"Allowed media types (`image/*`, `application/pdf`) and extensions (`.csv`); null means anything."}}},"created_at":{"type":"string","format":"date-time"},"add_files_until":{"type":"string","format":"date-time","description":"Ten minutes after creation, fixed. No new file can be reserved after it; the person can still send the files they added."},"expires_at":{"type":"string","format":"date-time","description":"The last moment the person can confirm (Send). Starts at `add_files_until`; it moves out when an upload finishes or an upload that was running ends without landing, never past 35 minutes after creation."},"upload_page_url":{"type":"string","description":"The Elaichi app page where the person confirms. Carries no secret: only the signed-in person who made the request can use it."},"files":{"type":"array","description":"At most `limits.max_files`.","items":{"type":"object","properties":{"file_id":{"type":"string"},"filename":{"type":"string"},"size_bytes":{"type":["integer","null"]},"mime":{"type":["string","null"]},"state":{"type":"string","enum":["uploading","staged","ready","stalled"]}}}}}}}}},"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 one upload request. It reports the request’s status, limits, deadline and files, to the person who made it. `404` for anyone else and for an id that does not exist, the same answer, so the page can say “this isn’t for this account” without confirming that a request exists.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Request id (`fupr_…`)."}]}},"/file/upload-request/{id}/file":{"post":{"operationId":"reserveFileUpload","summary":"Reserve a place for a file in an upload request","tags":["Files"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"object","properties":{"file_id":{"type":"string"},"filename":{"type":"string"},"size_bytes":{"type":["integer","null"]},"mime":{"type":["string","null"]},"state":{"type":"string","enum":["uploading","staged","ready","stalled"]}}}}}}}},"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":"Step one of adding a file: say what is about to be sent (`{ filename, size_bytes, mime? }`) and receive the slot id (`file.file_id`) to upload to and, at any moment, to cancel by. Refused unless the request is open **before its cutoff** (ten minutes after it was made; nothing ever moves it), has room (`409` `upload_request_full`), the size is within the request’s per-file limit (`413`) and the type within `accept` (`415`). Nothing is stored yet, and the slot is not a file: it appears in no list.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["filename","size_bytes"],"properties":{"filename":{"type":"string","maxLength":500},"size_bytes":{"type":"integer","minimum":0,"maximum":99614720},"mime":{"type":"string","maxLength":255}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Request id (`fupr_…`)."}]}},"/file/upload-request/{id}/file/{file_id}":{"put":{"operationId":"stageFileUpload","summary":"Send the bytes of a reserved file","tags":["Files"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"object","properties":{"file_id":{"type":"string"},"filename":{"type":"string"},"size_bytes":{"type":["integer","null"]},"mime":{"type":["string","null"]},"state":{"type":"string","enum":["uploading","staged","ready","stalled"]}}}}}}}},"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":"Uploads the bytes of a reserved file. The body is the file: send `Content-Type` and a `Content-Length` (`411` `upload_length_required` without one; it may not exceed the size reserved). One body per slot. Streamed into storage with the size counted on the bytes actually read (`413` `upload_too_large`, nothing kept) and the type read from the bytes: a file whose bytes are a specific type outside `accept` is refused `415` even when its name and header said otherwise. The file is STAGED, not yet a file: it appears in no list and no tool can use it until the request is confirmed. Any failure, or a `DELETE` of the slot while the body is still arriving, removes the slot and its bytes. An upload that began before the cutoff may finish; each finished file keeps the request confirmable for five more minutes, never past a hard ceiling of 35 minutes after it was made.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Request id (`fupr_…`)."},{"name":"file_id","in":"path","required":true,"schema":{"type":"string"},"description":"The slot id from the reservation."}]},"delete":{"operationId":"removeStagedFile","summary":"Take a staged file out of an upload request","tags":["Files"],"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":"Deletes the staged bytes and the slot: a file the person removed, an upload they canceled mid-flight, or one that stalled. Scoped to this request (a file id of another request is simply gone). Removing a file already gone is a success; on a request that is already confirmed it is `409` `upload_request_closed` (a confirmed file is deleted with `DELETE /file/{id}`).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Request id (`fupr_…`)."},{"name":"file_id","in":"path","required":true,"schema":{"type":"string"},"description":"The staged file’s id from the request."}]}},"/file/upload-request/{id}/confirm":{"post":{"operationId":"confirmFileUploadRequest","summary":"Confirm an upload request","tags":["Files"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"request_id":{"type":"string","description":"Request id (`fupr_…`)."},"status":{"type":"string","enum":["open","confirmed","expired"],"description":"`open`: waiting for the person (files may be added, removed, confirmed). `confirmed`: they approved; the staged files are now theirs. `expired`: the deadline passed unconfirmed with nothing still uploading. A request whose upload STARTED before the deadline stays open until that upload finishes, and each finished file keeps it open for at least five more minutes, so someone mid-upload is never told it expired."},"purpose":{"type":"string","description":"What the files are for, as the asker said it."},"requester":{"type":"string","description":"Who is asking: the connected app’s name, or “Elaichi assistant”."},"limits":{"type":"object","properties":{"max_files":{"type":"integer","description":"Files at most. 10."},"max_file_bytes":{"type":"integer","description":"The most one file may be. 95 MiB, or the request’s lower setting."},"max_total_bytes":{"type":"integer","description":"The most the whole request may hold. 250 MiB."},"accept":{"type":["array","null"],"items":{"type":"string"},"description":"Allowed media types (`image/*`, `application/pdf`) and extensions (`.csv`); null means anything."}}},"created_at":{"type":"string","format":"date-time"},"add_files_until":{"type":"string","format":"date-time","description":"Ten minutes after creation, fixed. No new file can be reserved after it; the person can still send the files they added."},"expires_at":{"type":"string","format":"date-time","description":"The last moment the person can confirm (Send). Starts at `add_files_until`; it moves out when an upload finishes or an upload that was running ends without landing, never past 35 minutes after creation."},"upload_page_url":{"type":"string","description":"The Elaichi app page where the person confirms. Carries no secret: only the signed-in person who made the request can use it."},"files":{"type":"array","description":"At most `limits.max_files`.","items":{"type":"object","properties":{"file_id":{"type":"string"},"filename":{"type":"string"},"size_bytes":{"type":["integer","null"]},"mime":{"type":["string","null"]},"state":{"type":"string","enum":["uploading","staged","ready","stalled"]}}}}}}}}},"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":"Confirms an upload request. The person’s explicit approval: the staged files become their files (origin `request`, kept 7 days) in one atomic step, and from then a tool may name them. Idempotent: confirming a confirmed request returns the same files and writes nothing. `409` `uploads_in_progress` while a file is still uploading, `no_files_staged` when nothing was added, `upload_request_closed` once it has expired. Audited as `file.upload_confirmed` (count and names) and one `file.uploaded` per file.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["file_ids"],"properties":{"file_ids":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":10,"description":"EXACTLY the staged files the person was shown. Any difference from what the server holds is `409` `upload_changed` and nothing is committed."}}}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Request id (`fupr_…`)."}]}},"/file/{id}":{"get":{"operationId":"getFile","summary":"Get a file’s status","tags":["Files"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"file_id":{"type":"string"},"status":{"type":"string","enum":["pending","ready","expired"],"description":"`pending`: an upload link you made is waiting for its bytes (not a file yet: no type, size or URL). `ready`: a file you can open. `expired`: your link ran out unused, or the file it filled is gone."},"filename":{"type":"string"},"mime":{"type":["string","null"]},"size_bytes":{"type":["integer","null"]},"expires_at":{"type":"string","format":"date-time"},"url":{"type":["string","null"],"description":"The Elaichi app’s download page, for a `ready` file."},"origin":{"type":"string","enum":["tool","upload","request","inline","api","url_import","web_fetch","export"],"description":"How the bytes got here: `tool` (a connector tool returned them), `upload` (a person chose it on the Files screen), `request` (a person handed it to an app that asked, through an upload request; `origin_host` is the app), `inline` (content an assistant wrote with `file.create`), `api` (a backend sent it to `POST /file` with a token), `url_import`, `web_fetch` (fetched from the web), `export` (produced by an export). Provenance for wording (“Uploaded by Dana”), never an access gate."},"origin_host":{"type":["string","null"],"description":"The web host an import or web fetch read from, or the asking app’s name for origin `request`; null otherwise."},"max_bytes":{"type":["integer","null"],"description":"The link’s size limit while `pending`."},"access":{"type":["string","null"],"enum":["owner","view",null]},"can_open":{"type":"boolean"},"can_delete":{"type":"boolean"},"can_share":{"type":"boolean"},"can_see_shares":{"type":"boolean"},"can_revoke_share":{"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":"Returns the status of one file id. The status is `pending` (their upload link is waiting for its bytes), `ready` (a file they can open, with the same `can_*` fields a list row carries) or `expired`. The poll for an upload, and the cheap “can I use this id?” check before passing it to a connector tool. `404` is the one answer for an id that is not the caller’s to know about: unknown, someone else’s, another organization’s, or vetoed by a restriction.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"File id (`file_…`)."}]},"delete":{"operationId":"deleteFile","summary":"Delete a tool file","tags":["Files"],"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":"Permanently deletes the file’s bytes, its record and every share on it. Irreversible, and it takes the file away from everyone it was shared with. Audited as `file.deleted`.\n\nOwner only: the person who ran the tool, not the owner of the connection behind it. Unlike the content route this one requires `X-Organization-Id` (it is a command, never a link someone clicks). A caller who is not the owner gets `403` (\"Only the owner…\") whenever the file is visible to them (an explicit share, or `use` on its source connection or toolbox, since it is in their `GET /file`); `404` for everyone else, including a malformed or unknown id, an expired file, and a member the file does not reach. A second delete of the same id is a `404`.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"File id (`file_…`)."}]}},"/file/{id}/share":{"get":{"operationId":"listFileShares","summary":"List file shares","tags":["Files"],"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":"Share id (`acl_…`), the `{aclId}` that `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"],"description":"A file grants exactly one level."},"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"]},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."},"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":"Lists who a file is shared with, cursor-paginated. This is the only place the grant list is served — a file can be shared organization-wide, so no row embeds it (`access_summary` on `GET /file` is the bounded rollup). Filter with `grantee_type`; `q` matches member names and emails and team names. Owner only: ownership is the gate, and there is no `file:share` permission. A caller who is not the owner gets `403` (\"Only the owner…\") whenever the file is visible to them in `GET /file` (an explicit share, or `use` on its source connection or toolbox, restricted or not), because they already know it exists; `404` otherwise, including a listed row whose access through a connection or toolbox has broken (`can_open: false` for a lapsed toolbox delegation or a deleted connection).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"File id (`file_…`)."},{"$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":"shareFile","summary":"Share a file","tags":["Files"],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/AclEntry"},{"$ref":"#/components/schemas/ShareManyResponse"}]}}}},"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, one team, or the whole organization the right to open and download the file. `level` must be `view` — a file has nothing to `use` and no settings to `edit`, and an `edit` grantee could re-share data pulled from a connection they may not see — so any other level is `400 invalid_share_level`. Re-granting the same grantee replaces the row rather than stacking one. The grantee must be an active member of this organization (or a team of it), and sharing with yourself is `403`, as on every other resource. A grantee opens the file with the same `GET /file/{id}/content` (and MCP `elaichi://file/{id}`) and sees it in `GET /file` with `access: \"view\"`; sharing never lets them share, revoke or delete it. Revoking, deleting or letting the file expire takes the access away. Audited as `file.shared`. Owner only: ownership is the gate, and there is no `file:share` permission. A caller who is not the owner gets `403` (\"Only the owner…\") whenever the file is visible to them in `GET /file` (an explicit share, or `use` on its source connection or toolbox, restricted or not), because they already know it exists; `404` otherwise, including a listed row whose access through a connection or toolbox has broken (`can_open: false` for a lapsed toolbox delegation or a deleted connection).","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ShareRequest"},{"$ref":"#/components/schemas/ShareManyRequest"}]}}}},"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"File id (`file_…`)."}]}},"/file/{id}/share/{aclId}":{"delete":{"operationId":"revokeFileShare","summary":"Revoke a file share","tags":["Files"],"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":"Revokes a share on a file. The grantee loses the file immediately — unless they still reach it through the connection or toolbox it came from, which a share revoke does not touch. Audited as `file.share_revoked`. A share id that does not belong to this file is `404 Share not found`. Owner only: ownership is the gate, and there is no `file:share` permission. A caller who is not the owner gets `403` (\"Only the owner…\") whenever the file is visible to them in `GET /file` (an explicit share, or `use` on its source connection or toolbox, restricted or not), because they already know it exists; `404` otherwise, including a listed row whose access through a connection or toolbox has broken (`can_open: false` for a lapsed toolbox delegation or a deleted connection).","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrganizationIdHeader"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"File id (`file_…`)."},{"name":"aclId","in":"path","required":true,"schema":{"type":"string"},"description":"Share id (`acl_…`) from the share list."}]}},"/file/{id}/content":{"get":{"operationId":"getFileContent","summary":"Download a tool file’s bytes","tags":["Files"],"responses":{"200":{"description":"The full file.","headers":{"Content-Disposition":{"schema":{"type":"string"},"description":"attachment; filename=\"…\""},"Content-Type":{"schema":{"type":"string"}},"Content-Length":{"schema":{"type":"integer"}},"X-Content-Type-Options":{"schema":{"type":"string"},"description":"nosniff"},"Accept-Ranges":{"schema":{"type":"string"}}},"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"206":{"description":"Partial content, honoring the `Range` request header.","headers":{"Content-Range":{"schema":{"type":"string"},"description":"bytes {start}-{end}/{total}"},"Content-Length":{"schema":{"type":"integer"}}},"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"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: the one answer for every refusal (unknown id, a file the caller cannot reach, access through a connection or toolbox that a restriction vetoes, a cross-org token, a caller who has left the org, an expired file, a blocked support session, a suspended org, or an org with no active plan)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"416":{"description":"The `Range` header cannot be satisfied (it starts at or past the end of the file). No body; `Content-Range` carries the real size.","headers":{"Content-Range":{"schema":{"type":"string"},"description":"bytes */{total}"}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"description":"Downloads the file. Streams the bytes (never JSON on success) with `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`. Supports `Range` (standard partial-content semantics; answers `206` with `Content-Range` when the request carries one).\n\n**Three things decide who may open a file, checked on every request:** (1) the owner: the member who ran the tool, always (deliberately not the owner of the connection behind it, because the person a file was made for routinely has no access to that connection); (2) anyone the owner shared it with (`POST /file/{id}/share`): a user, a team they are in, or the whole organization, at `view`; (3) anyone with `use` or better on the connection or the toolbox the file came from, because they could have produced the same output. The third follows those grants as they move (revoke someone’s toolbox share and the file goes with it) and is vetoed by the caller’s own organization restrictions on that connector: a member a restriction blocks from the connector cannot open its files through this route either. A file reached the third way is listed by `GET /file` (flagged `restricted_by` and `can_open: false` when a restriction blocks it), but this route stays the authority. No permission gates any of it: an org permission plays no part in a file’s reach, and org owners and admins get no extra read.\n\nSend no `X-Organization-Id` header: both a session cookie and an org API token work here, and an API token’s own organization is checked against the file’s organization, so a token minted in one org can never read a file in another, even when the same person is a member of both. The caller must also be a current member of the file’s org, not merely its creator back when the tool ran.\n\nEvery refusal answers the identical `404`, never `403`: a malformed or unknown id, a caller none of the three reaches, an access through a connection or toolbox that a restriction vetoes (or whose source connection has since been deleted), an API token pinned to another organization, a member who has left the org, an expired file, an Elaichi support session impersonating the owner in an org that turned support access off, a suspended org, and an org with no active plan. Existence is not disclosed to anyone the file does not reach, and the response never names which check failed. The one other refusal is `416` for a `Range` that starts at or past the end of the file.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"File id (`file_…`)."},{"name":"Range","in":"header","required":false,"description":"Standard HTTP byte-range request, e.g. `bytes=0-1023`.","schema":{"type":"string"}}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"session or API token","description":"`Authorization: Bearer <token>` with an org-scoped API token (`elch_{org_id}_{64 hex}`), created in the Elaichi app under Settings → API tokens. The Elaichi app itself signs in with a session cookie, which is also accepted but is not meant for integrations. 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. It can also ask an admin for access to something restricted for you.","mcp:write":"Create and change teams, roles, toolboxes, templates and connections, invite people, and share access.","mcp:destructive":"Permanently delete data and remove people in Elaichi. This cannot be undone.","mcp:tools":"Run tools from the toolboxes you choose next, deleting in your connected apps included. It can also ask an admin for access to something restricted for you.","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. `toolboxes_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":"True only for Elaichi support accounts. Always false for customer members; 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":"Set when Elaichi granted this organization an indefinite trial, otherwise 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. Server-computed, so branch on this rather than inspecting roles. Always `false` on rows that have no member context to compute it from, such as the organization list `GET /organization`. It never overstates: trust it when true, and read `GET /user/me` or `GET /organization/{id}` (both compute it) when you need it for a list row."},"can_manage":{"type":"boolean","description":"Whether the caller may change this organization's settings: the `org:manage` permission that `PATCH /organization/{id}` requires, and the organization is neither suspended nor in its deletion grace period. Server-computed, and `false` wherever `can_delete` is false for lack of a member context; it never overstates."},"allow_staff_impersonation":{"type":"boolean","description":"Whether Elaichi support may sign in as this organization's members to help them. `true` by default. When `false`, every request made through such a support session against this organization is refused with `403 staff_impersonation_blocked`, reads included, and the organization is left out of that session's `GET /user/me` and `GET /organization`. Changed with `PATCH /organization/{id}` (`org:manage`)."},"mfa_required":{"type":"boolean","description":"Whether members must hold a second factor to act in this organization. `false` by default. When `true`, a signed-in session that has not shown a second factor (no TOTP challenge at login, no passkey sign-in, no factor enrolled since) is refused on every organization-scoped route, reads included, with `403 mfa_setup_required`; `error.details.has_second_factor` says whether the person already has one and only needs to sign in with it. Exempt: org API tokens, Elaichi support sessions, and a session signed in through this organization's own SSO connection. MCP connections made before it was switched on, or from a session without a factor, are refused until the person connects again. Turning it off only lifts the organization requirement: a member who set up an authenticator app of their own is still challenged for it at every non-passkey sign-in. Changed with `PATCH /organization/{id}` (`org:manage`, interactive session only)."},"can_require_mfa":{"type":"boolean","description":"Whether the CALLER may turn `mfa_required` ON right now: `can_manage`, from an interactive session that would itself pass the check it is about to create. `false` — with the reason in `can_require_mfa_reason` — when switching it on would lock the caller out of this organization. The PATCH refuses that regardless with `409 mfa_setup_required`. Turning it OFF is not governed by this field; it needs a step-up."},"can_require_mfa_reason":{"type":["string","null"],"description":"A sentence saying why `can_require_mfa` is `false` for a caller who can otherwise manage the organization. `null` otherwise."},"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` on rows with no member context. Read `purge_after` to tell \"too late\" from \"not yours to undo\"."},"can_authorize_apps":{"type":"boolean","description":"Whether the CALLER may connect an app (MCP OAuth consent) to this organization right now: `false` while it is suspended, scheduled for deletion, or has no active plan. Server-computed by the same gate `POST /oauth/authorize-request/{id}/approve` calls; present only on member-scoped responses. `authorize_apps_blocked_reason` says which."},"authorize_apps_blocked_reason":{"type":["string","null"],"enum":["blocked","pending_deletion","no_plan",null],"description":"Why `can_authorize_apps` is `false`, in the gate's own order: `blocked`, then `pending_deletion` (even when the subscription is also gone, since a soft delete cancels it), then `no_plan`. `null` when connecting is open."},"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":"Every permission the role holds, including any `GET /permission` does not list for this organization (for example the Apps permissions while Apps are unavailable). `PATCH /role/{id}` replaces this list wholesale, so send back the entries you did not mean to remove."},"permission_count":{"type":"integer","description":"How many of `permissions` `GET /permission` lists for this organization: the number to show beside the role. Can be lower than `permissions.length`."},"member_count":{"type":"integer","description":"How many members hold this role right now. `GET /member?role_id={id}` lists them; while it is above zero, `DELETE /role/{id}` needs `reassign_to` (or, on `POST /role/{id}/delete`, `reassign_to` and/or a `reassignments` entry for every holder)."},"pending_invite_count":{"type":"integer","description":"Pending invites issued for this role. Present only on roles you can manage. While above zero, `DELETE /role/{id}` and `POST /role/{id}/delete` need `reassign_to`, which re-points them."},"group_mapping_count":{"type":"integer","description":"SCIM group mappings that assign this role. Present only on roles you can manage. While above zero, `DELETE /role/{id}` is refused (`409 role_mapped`) until the mappings change."},"can_reassign_holders":{"type":"boolean","description":"Whether you may move this role's holders and pending invites to another role (`DELETE /role/{id}?reassign_to=` or `POST /role/{id}/delete`): you hold `member:manage` and do not hold this role yourself."},"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. The row carries no roster array; `GET /team/{id}/member` is the roster."},"admin_count":{"type":"integer","description":"How many of them administer the team."},"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"]},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."},"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"]},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."},"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"]},"avatar_url":{"type":["string","null"],"description":"The person's picture: their stored profile picture, else a Gravatar URL (`d=404`, 96px) derived from their email (the address itself is not sent), else null. Draw initials when it is null or the image fails to load."},"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 access grant on a connection, toolbox, template, connector or (at `view` only) a stored tool file.","properties":{"id":{"type":"string","description":"Grant id (`acl_…`), the `{aclId}` that `DELETE .../share/{aclId}` takes."},"resource_type":{"type":"string","enum":["connection","toolbox","template","connector","file"]},"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. 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"}}},"ShareManyRequest":{"type":"object","required":["grantees","level"],"description":"Share with several grantees at one level. Partial success: each grantee is granted or refused on its own, and only a request where nobody is granted answers 400 `share_grantees_rejected` (every reason in `error.details.rejected`). Duplicates collapse to one grant.","properties":{"grantees":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["grantee_type"],"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"}}},"ShareManyResponse":{"type":"object","description":"The answer to a `grantees` share: the grants as written, and everyone who was not granted with the code the single form would have answered (`share_target_is_self`, `share_target_is_owner`, `member_not_active`, `bad_request`). A resource's own extra fields (a toolbox's `delegation`) ride alongside.","properties":{"result":{"type":"array","items":{"$ref":"#/components/schemas/AclEntry"}},"rejected":{"type":"array","items":{"type":"object","properties":{"grantee_type":{"$ref":"#/components/schemas/GranteeType"},"grantee_id":{"type":["string","null"]},"code":{"type":"string"},"message":{"type":"string"}}}}},"additionalProperties":true}}}}