{"name":"UnlikeOtherAuthenticator","description":"Centralized OAuth and authentication service used by multiple products, including one-time first-hop assertions, audience-bound chained confidential exchange, and paged organisation membership rosters.","version":"0.0.0","repository":"https://github.com/UnlikeOtherAI/UnlikeOtherAuthenticator","home":"/","docs":"/llm","api":"/api","native_apps":{"admin":"/admin/apps","registration":"/oauth/register","public_identifier":"app_id","client_secret_required":false},"kelpie_connect":{"issue":"/oauth/kelpie/connect","validate":"/oauth/kelpie/connect/validate","scope":"kelpie.connect","app_id":"com.unlikeotherai.kelpie","ticket_ttl_seconds":120,"configuration":"KELPIE_CONNECT_RESOURCE; exact HTTPS /api/kelpie/connect gateway, absent disables"},"kelpie_pair":{"issue":"/oauth/kelpie/pair","validate":"/oauth/kelpie/pair/validate","scope":"kelpie.connect","receiver_scope":"profile","ticket_ttl_seconds":120,"same_subject_required":true},"admin_team_membership":"POST /internal/admin/users/:userId/teams atomically adds an existing user to the selected organisation/team through the admin superuser boundary; see endpoints for its request and lifecycle rules.","ledger_runtime_keys":"Admin Billing > selected product > Ledger runtime keys; existing platform-superuser metadata/issue/revoke API, one-time plaintext only, no installation or bootstrap authority.","sms_pricing":"UOA owns final mobile-number and SMS quotes at /billing/v1/sms/quotes and /verify. Provider configuration and accepted dated currency policy are required; customer payloads contain final prices only.","sms_policy_operators":"Admin Billing > SMS policies provides fixed-source ECB refresh/XML import, dimensional route evidence review and immutable acceptance. Current platform SUPERUSER only; five-minute operator/epoch-bound previews, private/no-store. SMS runtime credentials are issued under the selected product App keys with purpose sms_runtime.","config_jwt":{"description":"The config JWT is a signed JWT containing all client-specific settings. The payload is the config. The signature must be RS256 and the protected header must include kid.","signing":{"algorithms":["RS256"],"key_selection":"The JWT protected header must include kid. The auth service resolves that kid from CONFIG_JWKS_URL and rejects tokens without kid."},"required_fields":{"domain":"string — client service identity: a hostname, or hostname/lowercase-mount-path without a trailing slash for an independent subfolder service. The complete value owns its separate keys, credentials, roles and policies. Config and JWKS URLs must match the hostname and stay within the exact mount path. Legacy hostname clients are unchanged.","redirect_urls":"string[] — non-empty list of callback targets (RFC 8252): an https:// URL on any host, an http:// URL on loopback only (localhost/127.0.0.1/[::1]), or a native custom-scheme deep link (e.g. com.acme.app://callback). Non-loopback http and dangerous schemes (javascript:, data:, …) are rejected. The runtime redirect_url must match an entry byte-for-byte, including scheme, host, port, path, AND query string. No normalization, no prefix matching, no query wildcards. Do NOT append per-request state (?state=…) to the redirect — carry it out-of-band via sessionStorage or a first-party cookie.","enabled_auth_methods":"string[] — non-empty list. Supported values are email_password, google, facebook, github, linkedin, apple. Social provider names here are both enabled and allowed.","language_config":"string | string[] — either one language code or a non-empty array of language codes.","ui_theme":{"description":"object — every auth page style comes from ui_theme. This object is required and the main source of integration mistakes.","required_sections":{"colors":{"required_keys":["bg","surface","text","muted","primary","primary_text","border","danger","danger_text"],"value_format":"hex color only (#RGB, #RGBA, #RRGGBB, #RRGGBBAA) or transparent"},"radii":{"required_keys":["card","button","input"],"value_format":"CSS length string: px, rem, em, %, or 0"},"density":"compact | comfortable | spacious","typography":{"required_keys":["font_family","base_text_size"],"font_family":"Preset values like sans, serif, mono are valid. Custom font names must match /^[A-Za-z0-9 _-]+$/.","base_text_size":"sm | md | lg","font_import_url":"optional HTTPS stylesheet URL. The Auth UI silently drops font imports outside fonts.googleapis.com, fonts.gstatic.com, and fonts.bunny.net."},"button":{"required_keys":["style"],"style":"solid | outline | ghost"},"card":{"required_keys":["style"],"style":"plain | bordered | shadow"},"logo":{"required_keys":["url","alt"],"url":"HTTPS URL on the same host as config.domain, or empty string","alt":"required non-empty string","rounded":"optional boolean (default true) — gives an image logo gently rounded corners using ui_theme.radii.button; set false to preserve square corners. Ignored for text logos.","text":"optional text logo, max 100 chars, used when url is empty","font_size":"optional CSS length string","color":"optional hex color","style":"optional flat object. The Auth UI applies only color, fontSize, fontWeight, and letterSpacing."}},"optional_sections":{"css_vars":"Record<string, string> — optional advanced CSS variable overrides."},"common_failures":["Missing one of the required ui_theme sections such as colors, radii, button, card, typography, or logo.","Using non-hex colors like rgb(...), hsl(...), or named colors.","Using bare numbers for radii/font sizes instead of CSS length strings like 12px or 1rem.","Providing a text logo without logo.alt.","Forgetting button.style or card.style."],"example":{"colors":{"bg":"#f8fafc","surface":"#ffffff","text":"#0f172a","muted":"#475569","primary":"#2563eb","primary_text":"#ffffff","border":"#e2e8f0","danger":"#dc2626","danger_text":"#ffffff"},"radii":{"card":"16px","button":"12px","input":"12px"},"density":"comfortable","typography":{"font_family":"sans","base_text_size":"md"},"button":{"style":"solid"},"card":{"style":"bordered"},"logo":{"url":"","alt":"Client logo","rounded":true,"text":"Client","font_size":"24px","color":"#0f172a","style":{"fontWeight":"800","letterSpacing":"0.02em"}}}}},"optional_fields":{"2fa_enabled":"boolean (default false) — master gate for 2FA. When false or absent, effective 2FA policy is off regardless of Admin policy. When true, DB-backed ClientDomain and Organisation policies resolve strongest-wins: off < optional < required.","debug_enabled":"boolean (default false)","user_scope":"\"global\" | \"per_domain\" (default \"global\")","allow_registration":"boolean (default true)","existing_user_registration_behavior":"\"email_login_link\" | \"inline_sign_in\" (default \"email_login_link\") — opt-in override for clients that prefer an inline already-registered sign-in prompt over sending a login-link email during registration","registration_mode":"\"password_required\" | \"passwordless\" (default \"password_required\")","allowed_registration_domains":"string[] — lowercase email domains allowed to register","registration_domain_mapping":"array of { email_domain, org_id, team_id? } — email-domain-based org/team placement","language":"string — currently selected language override","login_flow":{"email_code_enabled":"boolean (default false)","team_selection":"\"off\" | \"auto\" (default \"off\") — \"auto\" resolves team after identity verification. Legacy clients use ACTIVE org+team memberships on their own config domain. A server-recognized product domain (active ClientDomain plus one unambiguous active/current CUSTOMER_LIFECYCLE app-key service mapping with exact HTTPS actorIssuer) uses all exact ACTIVE org+team memberships; unknown/revoked/ambiguous mappings remain same-domain and pending invites are always same-domain. Exactly one eligible membership with no pending invite is selected server-side and bound through applicable 2FA to the authorization code/active claim; multiple teams, any invite, or zero teams with can_create_org preserve the chooser. \"off\" suppresses the chooser, but a recognized product still resolves one exact team (or required first placement) before 2FA and fails closed on ambiguity; only legacy non-product login remains unscoped. The exact selected Organisation policy participates in strongest-wins even across domains. The chooser login_token cryptographically binds the exact verified config URL+semantics, redirect, PKCE, remember-me, and access-request continuation and is consumed once with the final selection. An accepted email invite is already an explicit selection and carries its exact ACTIVE org/team through 2FA."},"avatars":{"default_style":"\"tiles\" | \"waves\" | \"rings\" | \"mono\" (optional) — the domain default style for UOA-generated fallback avatars. Absent means a stable per-user pick derived from the user id. A `?style=` query parameter on the avatar GET endpoints overrides this. Any other value rejects the config."},"session":{"remember_me_enabled":"boolean (default true)","remember_me_default":"boolean (default true)","short_refresh_token_ttl_hours":"number (1-168, default 1)","long_refresh_token_ttl_days":"number (1-90, default 30)","access_token_ttl_minutes":"number (15-60)"},"org_features":{"enabled":"boolean (default false)","groups_enabled":"boolean (default false)","user_needs_team":"boolean (default false)","auto_create_personal_org_on_first_login":"boolean (default false) — create a personal org + default team on first verified login when no mapping matches","allow_user_create_org":"boolean (default false) — permit end-users to call POST /org/organisations with their own access token (superusers bypass)","allow_user_create_team":"boolean (default false) — permit an ACTIVE org owner/admin to create a FURTHER team (team) in that organisation from the SSO chooser via POST /auth/create-team. Separate from allow_user_create_org (a new organisation): this writes into an existing tenant, so the domain opts in and the owner/admin role check still applies","backend_org_management":"boolean (default false) — permit this domain's product backend to call /org/* with NO X-UOA-Access-Token, authorised by the domain pairing (domain-hash bearer + this signed config) alone. There is then no acting user, so per-user org-role checks do not apply and every mutation is attributed to the backend in the org audit log","pending_invites_block_auto_create":"boolean (default true) — a pending invite for the user email suppresses auto_create_personal_org_on_first_login","max_teams_per_org":"number (default 100, max 1000)","max_groups_per_org":"number (default 20, max 200)","max_members_per_org":"number (default 1000, max 10000)","max_members_per_team":"number (default 200, max 5000)","max_members_per_group":"number (default 500, max 5000)","max_team_memberships_per_user":"number (default 50, max 200)","org_roles":"string[] (default [\"owner\", \"admin\", \"member\"]). Must include \"owner\".","team_roles":"string[] (default [\"owner\", \"admin\", \"member\"]). Must include \"owner\". The mirror of org_roles for team membership: team roles used to be fixed in code, and this makes them the domain's to name. Validated on every team-role write","member_invitable_team_roles":"string[] (default []) — explicit non-owner team roles a non-manager may assign through a member-initiated invite. member_invites decides whether members may invite; this list decides the authority an invite can grant. Omitted teamRole selects the first configured role, so an empty list rejects member-initiated invites until the domain opts in.","capabilities":"string[] (optional) — the consuming product's declared capability catalogue, mirrored here so role_grants can be validated against it. UOA's own names (members.manage, teams.manage, organisation.manage) are always valid on top of this list","role_grants":"object (optional) — { \"org\": { <role>: <capability>[] }, \"team\": { <role>: <capability>[] } }. Which roles hold which capabilities; UOA gates its own org and team surfaces on capability names, never role strings. Its own catalogue: members.manage (roster mutation at either scope — add/remove/deactivate/reactivate an org member, add/remove/re-role a team member, invitations, invite links, the invited list), teams.manage (the team object: create, rename, re-slug, join policy, icon, delete), organisation.manage (the organisation object: rename, member-invites policy, icon; ORG SCOPE ONLY — those gates never read a team role, so a role_grants.team entry naming it is inert). Rules: \"owner\" (org or team) implicitly holds every capability and MUST NOT appear here; a role the table does not mention holds nothing (no coercion to \"member\"); a team answer is the union of the org-role and team-role grants, so an org grant of a team-scope capability reaches into every team. Deliberately not capabilities, because they are structural: organisation delete, ownership transfer, changing an org member role (the actor must BE Organisation.ownerId), granting or removing the \"owner\" role, and billing management (a UOA-computed verdict). Rejected at config load if a role is not in the matching vocabulary, a capability is undeclared, \"owner\" is named, or the scope key is unknown. Absent means the legacy default table: {\"org\":{\"admin\":[\"members.manage\",\"teams.manage\",\"organisation.manage\"]},\"team\":{\"admin\":[\"members.manage\",\"teams.manage\"]}}","max_flags_per_app":"number (default 100, max 500)","scim_override_retention":"\"retain\" | \"clear\" (default \"retain\")","global_missing_flag_default":"\"enabled\" | \"disabled\" (default \"disabled\")"},"access_requests":{"enabled":"boolean (default false)","target_org_id":"string (required when enabled=true)","target_team_id":"string (required when enabled=true)","auto_grant_domains":"string[]","notify_org_roles":"string[] (default [\"owner\", \"admin\"])","admin_review_url":"absolute URL"}},"example_payload":{"domain":"client.example.com","redirect_urls":["https://client.example.com/oauth/callback"],"enabled_auth_methods":["email_password","google"],"ui_theme":{"colors":{"bg":"#f8fafc","surface":"#ffffff","text":"#0f172a","muted":"#475569","primary":"#2563eb","primary_text":"#ffffff","border":"#e2e8f0","danger":"#dc2626","danger_text":"#ffffff"},"radii":{"card":"16px","button":"12px","input":"12px"},"density":"comfortable","typography":{"font_family":"sans","base_text_size":"md"},"button":{"style":"solid"},"card":{"style":"bordered"},"logo":{"url":"","alt":"Client logo","rounded":true,"text":"Client","font_size":"24px","color":"#0f172a","style":{"fontWeight":"800","letterSpacing":"0.02em"}}},"language_config":"en","debug_enabled":true,"allow_registration":true,"registration_mode":"password_required","user_scope":"global"}},"website_service_identity":"Separate subfolder services use config.domain=hostname/mount-path (no trailing slash). Register each complete identity independently; credentials, signing keys, roles, allowlists and organisation origin scope never inherit from its parent hostname.","access_token":{"description":"Authorization-code, refresh, and explicit team-switch grants at POST /auth/token return the legacy HS256 JWT signed with SHARED_SECRET. The confidential RFC 8693 token-exchange grant is separate: it returns a 5-minute, resource-bound RS256 token verified with GET /oauth/jwks.json. Never apply one profile’s verification rules to the other.","signing":{"profile":"legacy authorization-code, refresh-token, and team-switch grants only","algorithm":"HS256","key":"deployment-wide SHARED_SECRET (not exposed, no UOA-side public JWKS)","audience":"uoa:access-token"},"claims":{"sub":"string — stable external user id. Primary foreign key for the RP into the UOA user.","email":"string — user primary email. Advisory (user may change it); sub is the stable identity.","role":"string — UOA platform role, \"user\" or \"superuser\". Gates UOA admin surfaces only. DO NOT use for RP authorization; honour firstLogin.memberships.orgs[].role (or GET /org/me) instead.","domain":"string — integration domain from the config JWT. Confirms which integration minted this token.","client_id":"string — SHA256(domain + clientSecret) hex. Identifies the exact client credential used.","org":"object | absent — present when org_features.enabled and the user has a live org context. This is normally same-domain; an exact team-scoped session for a server-recognized all_active_memberships product carries its selected cross-domain org instead. Shape: { org_id, org_role, teams[], team_roles{}, groups?[], group_admin?[] }.","iss":"string — UOA host (e.g. authentication.unlikeotherai.com).","aud":"string — always \"uoa:access-token\".","iat":"number — issued at, epoch seconds.","exp":"number — expiry, epoch seconds."},"rp_guidance":["Do NOT read response.user — there is no top-level user field. Decode access_token and read claims.sub.","Do NOT verify access_token against the /.well-known/jwks.json JWKS; that JWKS is for RS256 config JWTs only.","Do NOT use claims.role for RP authorization; it is the UOA platform role. Use firstLogin.memberships.orgs[].role (or GET /org/me).","Do NOT fall back to a synthetic tenant when firstLogin.memberships.orgs is empty — branch on firstLogin.capabilities (can_create_org / can_accept_invite). See /llm Phase 4.5.","Keep access_token server-side when possible. If forwarded to UOA-owned endpoints, use the X-UOA-Access-Token header."],"response_envelope_casing":"The /auth/token response envelope is snake_case (access_token, refresh_token, expires_in, refresh_token_expires_in, token_type). The key firstLogin is camelCase; memberships.orgs[].orgId, memberships.teams[].teamId, memberships.teams[].orgId, and pending_invites[].inviteId/orgId/teamId/teamName are camelCase. pending_invites and capabilities.can_* remain snake_case."},"team_switch":{"endpoint":"POST /auth/token?config_url=<verified config URL>","description":"Backend-only explicit rotation of one renewable legacy session to one exact currently authorized organisation/team. It is the only token grant that may change team scope; ordinary refresh is strictly scope-preserving.","request":{"grant_type":"urn:unlikeotherai:params:oauth:grant-type:team-switch","refresh_token":"opaque current or in-grace predecessor refresh token","organization_id":"exact target organisation id","team_id":"exact target team id within organization_id"},"operation_id":"Not accepted — deterministic predecessor/target binding is the operation identity.","assurance":"UOA rechecks the exact target ACTIVE organisation and team memberships, server-owned product scope policy, target effective 2FA policy, user enrollment, and signature policy before any rotation. RefreshToken.twoFaCompleted is copied from the one-time authorization code and remains immutable for the complete family; legacy rows default false.","response":"The normal access_token + refresh_token envelope, scoped exactly to organization_id/team_id, with no firstLogin block. Persist the pair atomically before using it.","replay":"For 120 seconds, an exact retry may recover the deterministic successor only while every descendant retains the requested target and its current policy still passes. A different grant/target or any later scope transition returns TEAM_SWITCH_CONFLICT without consuming or revoking a valid family. If the matching edge committed but its target policy is now unavailable or needs stronger assurance, UOA retires only that family and returns authenticated INVALID_REFRESH_TOKEN so a lost-response caller can clear its intent safely. Post-grace predecessor reuse always takes theft revocation. An ordinary refresh never returns a changed scope.","errors":{"TEAM_NOT_AVAILABLE":"403 — the exact pair is unavailable or product policy denies it; intentionally does not distinguish why","INTERACTION_REQUIRED":"403 — the family lacks the target policy’s current interactive assurance; restart authorization","TEAM_SWITCH_CONFLICT":"409 — same-scope no-op or a valid deterministic successor belongs to another grant/target","INVALID_REFRESH_TOKEN":"401 — after domain-client authentication succeeds, invalid, expired, revoked, structurally corrupt, or post-grace reused source; the stable code is exposed but the underlying reason remains opaque"}},"confidential_token_exchange":{"description":"Confidential RFC 8693-style exchange on POST /auth/token. Every calling product authenticates with its own existing per-domain app credential and selects one enabled DB mapping bound to that ClientDomain, product, exact resource, and scope allowlist; there is no shared credential or singleton env fallback. A first hop supplies a short-lived source-signed JWT assertion. A later app-to-app hop may instead submit the already UOA-issued access token whose audience is exactly that authenticated next-hop app. Both profiles re-resolve current identity and membership before UOA issues a narrowed, resource-bound RS256 access token.","request":{"grant_type":"urn:ietf:params:oauth:grant-type:token-exchange","subject_token":"Either a source-signed one-time RS256 JWT assertion carrying optional active team context, or a UOA-issued RS256 at+jwt access token already bound to the authenticated calling app with mandatory org/active context","subject_token_type":"urn:ietf:params:oauth:token-type:jwt for the first hop, or urn:ietf:params:oauth:token-type:access_token for a chained hop","product":"lowercase product identifier; must match the mapping bound to the authenticated ClientDomain","resource":"exact HTTPS resource URI stored in that product mapping","scope":"space-delimited exact requested subset of ai.invoke, billing.read, token.provision, memory.read, and/or memory.write; token provisioning is never implied by ai.invoke and UOA rejects any scope outside the mapping allowlist"},"application_binding":{"authentication":"the normal domain-hash bearer for the immediate caller config domain; credential rotation preserves the ClientDomain-bound mapping","mapping":"DB-backed exact ClientDomain + product -> HTTPS resource + supported-scope allowlist; unknown, disabled, cross-domain, cross-product, wrong-resource, and widening requests fail closed","administration":"audited superuser CRUD at /internal/admin/confidential-delegations; responses never expose credential material"},"subject_assertion_binding":{"issuer":"exact source config domain","audience":"PUBLIC_BASE_URL + /auth/token","key_source":"same-host jwks_url claim from the verified source config JWT","replay_protection":"A fresh unique jti is mandatory. After current identity and optional team validation, UOA atomically consumes source-domain+jti once in PostgreSQL through exp plus clock tolerance; exact/concurrent replay is rejected across instances."},"chained_access_token_binding":{"authentication":"the immediate caller still presents only its own per-domain app credential; another product credential, a webhook secret, a user token, or a shared fallback is never accepted as caller authentication","verification":"the subject must be a UOA RS256 at+jwt with iss equal to UOA and aud exactly https://<authenticated caller config domain>; source_domain and azp must identify the same inbound source product domain","team":"org and active are mandatory and internally consistent. UOA revalidates the inbound source-product mapping, then checks the stable user, source-domain role, ACTIVE organisation, and ACTIVE selected team under the ultimate signed origin at the tail of the act chain.","narrowing":"requested scope must be allowed by the immediate caller mapping and be a subset of the inbound token scope. The downstream expiry is capped to the remaining inbound lifetime.","reuse":"Unlike a source-signed JWT assertion, an audience-bound access-token subject is reusable until exp so concurrent multi-process calls do not turn a normal bearer token into a one-shot credential."},"errors":{"TOKEN_EXCHANGE_SUBJECT_FORBIDDEN":"403 — public code; the subject cannot be exchanged now because of the person’s own current state: a moved credential epoch (`tv`), an unknown user, a lost source-domain role, or a selected organisation/team no longer available to them. Every reason shares this one code; ask the person to sign in again. The body is exactly {\"error\":\"Request failed\",\"code\":\"TOKEN_EXCHANGE_SUBJECT_FORBIDDEN\"}","configuration":"403 with the generic body {\"error\":\"Request failed\"} and no code — the calling product’s own request or configuration was refused: no enabled delegation mapping for the product/resource/scope, scope widening, an assertion without team context where the product’s team policy requires one, or team context where the product supports none. These are decided before any subject lookup, so they are identical for every subject; treat them as an integration fault, not a sign-in problem."},"issued_access_token":{"algorithm":"RS256","lifetime":"maximum 300 seconds; a chained result never outlives its inbound access token","jwks":"GET /oauth/jwks.json","claims":"iss, aud, sub, email (advisory), source_domain, azp (immediate caller domain only), product, exact requested scope, jti, iat, exp; org and active are present together for a validated team and preserve the revalidated original tenancy. Chained tokens add an RFC 8693 act chain containing upstream source/product provenance.","forbidden_claims":"client_id and the 64-character domain-hash bearer credential are never copied into this token"},"rate_limit":"Confidential exchange is isolated from the legacy 10/min/IP token bucket: 600/min per authenticated caller domain, plus 60/min per verified caller-domain user.","public_oauth_boundary":"The signing key publishes GET /oauth/jwks.json but does not enable public registration, authorize, login, or PKCE token routes. Those require MCP_OAUTH_PUBLIC_PROFILE_ENABLED=true."},"config_validation":{"path":"/config/validate","method":"POST","description":"Production-safe configuration validation endpoint for raw config JSON, a signed config JWT, or a config_url fetch target. It reports schema, signature, config_url/domain, runtime-policy, and optional customization guidance.","auth":"Public, IP rate limited. Uses the deployment CONFIG_JWKS_URL for config JWT signature verification.","body":{"config":"object (optional) — raw config payload to schema-validate directly. This skips JWT signature checking unless config_jwt or config_url is also supplied instead.","config_jwt":"string (optional) — signed config JWT to decode, inspect, schema-validate, and verify with a JWKS.","config_url":"string (optional) — URL that should return the signed config JWT. The endpoint fetches it and then runs the same checks."},"source_priority":["config","config_jwt","config_url"],"response":{"ok":"boolean — true when every executed check passed","schema_valid":"boolean","jwt_signature_valid":"boolean | null — null when signature checking was skipped","domain_match":"boolean | null — null when config_url was not part of the request or schema parsing failed","checks":"object — per-stage results for source, fetch, decode, secret_scan, signature, schema, runtime_policy, and domain_match","issues":"array — structured stage-specific failures and warnings","recommendations":"array — required next steps, operational notes, and optional customization guidance","config_summary":"object | null — safe summary of the parsed config when schema validation succeeds"}},"config_verification":{"path":"/config/verify","method":"POST","description":"Non-production DEBUG_ENABLED-only debug endpoint that validates raw config JSON, a signed config JWT, or a config_url fetch target. It also accepts a jwks_url override for local setup debugging.","auth":"Available only when DEBUG_ENABLED=true and NODE_ENV is not production; IP rate limited.","body":{"config":"object (optional) — raw config payload to schema-validate directly. This skips JWT signature checking unless config_jwt or config_url is also supplied instead.","config_jwt":"string (optional) — signed config JWT to decode, inspect, schema-validate, and verify with a JWKS.","config_url":"string (optional) — URL that should return the signed config JWT. The endpoint fetches it and then runs the same checks.","jwks_url":"string (optional) — JWKS URL used to verify config_jwt or the JWT fetched from config_url. Defaults to CONFIG_JWKS_URL"},"source_priority":["config","config_jwt","config_url"],"response":{"ok":"boolean — true when every executed check passed","schema_valid":"boolean","jwt_signature_valid":"boolean | null — null when signature checking was skipped","domain_match":"boolean | null — null when config_url was not part of the request or schema parsing failed","checks":"object — per-stage results for source, fetch, decode, secret_scan, signature, schema, runtime_policy, and domain_match","issues":"array — structured stage-specific failures and warnings","recommendations":"array — required next steps, operational notes, and optional customization guidance","config_summary":"object | null — safe summary of the parsed config when schema validation succeeds"}},"endpoints":[{"method":"POST","path":"/oauth/lifecycle-status/start","notes":"Optional diagnostics: AUTH_ACCESS_STATUS_ENABLED=true on the auth server is required. False/unset returns404 before config/client/mail/proof work, including outstanding proof redemption.","auth":"registered public client_id and exact redirect_uri; rate limited","query":{"client_id":"registered client","redirect_uri":"exact registered callback"},"body":{"email":"valid email"},"description":"Begin read-only mailbox proof for native/public OAuth account reasons; same neutral delivery behavior as website proof"},{"method":"POST","path":"/oauth/lifecycle-status/verify","notes":"Optional diagnostics: AUTH_ACCESS_STATUS_ENABLED=true on the auth server is required. False/unset returns404 before config/client/mail/proof work, including outstanding proof redemption.","auth":"mailbox code and enrolled TOTP; current registered app revision","query":{"client_id":"same registered client","redirect_uri":"same exact callback"},"body":{"challengeId":"UUID","code":"6digits","twoFactorCode":"6digits if enrolled"},"description":"Read reasons only; no session or credential grant; revision change invalidates proof"},{"method":"GET","path":"/internal/admin/lifecycle/templates","auth":"live platform superuser","description":"List reusable scoped customer-facing disable reasons and revisions"},{"method":"POST","path":"/internal/admin/lifecycle/templates","auth":"live platform superuser","description":"Create or revise a reason template","body":{"id":"optional existing id","scope":"USER | ORGANISATION | TEAM","title":"1–120 characters","message":"1–2000 plain-text characters","enabled":"boolean"}},{"method":"GET","path":"/internal/admin/lifecycle/:scope/:id","auth":"live platform superuser","description":"Read own lifecycle, customer reason, internal note, template revision and durable deletion job id"},{"method":"POST","path":"/internal/admin/lifecycle/:scope/:id","auth":"live platform superuser","description":"Disable or reactivate access independently of membership state","body":{"status":"ACTIVE | DISABLED","templateId":"required for disable","templateRevision":"required exact revision for disable","internalNote":"optional administrator-only text, max2000"}},{"method":"POST","path":"/internal/admin/lifecycle/:scope/:id/deletion-preview","auth":"live platform superuser","description":"Preview team/org cascade, candidate account dependencies, product participants, active financial blockers and restricted evidence","body":{"mode":"RETAIN_REFERENCE | ERASE_REFERENCE"}},{"method":"POST","path":"/internal/admin/lifecycle/:scope/:id/delete","auth":"live platform superuser","description":"Confirm a current preview and start durable terminal deletion","body":{"mode":"RETAIN_REFERENCE | ERASE_REFERENCE","previewDigest":"current preview SHA256","confirmation":"exact preview confirmation string","requestKey":"UUID reused for retries"}},{"method":"GET","path":"/internal/admin/lifecycle/deletion-jobs/:id","auth":"live platform superuser","description":"Read durable progress and product acknowledgements"},{"method":"POST","path":"/internal/admin/lifecycle/deletion-jobs/:id/retry","auth":"live platform superuser","description":"Execute or retry local cleanup once all expected products acknowledge; bounded leased stages commit progress; retry READY to continue; COMPLETE is idempotent"},{"method":"GET","path":"/domain/deletion-jobs","auth":"exact registered domain bearer","query":{"domain":"exact registered product identity"},"description":"Pull up to100 pending jobs assigned to this ClientDomain, without unrelated PII","response":{"data":"id, revision, scope, targetId, effectiveScope, effectiveTargetId, mode, organisationId, teamIds, accountsToDelete"}},{"method":"POST","path":"/domain/deletion-jobs/:id/acknowledge","auth":"exact registered domain bearer","query":{"domain":"exact registered product identity"},"description":"Acknowledge exact job revision after committed product cleanup; replay must match outcome","body":{"revision":"positive integer","outcome":"PURGED | RETAINED_EVIDENCE","retainedEvidence":"optional max30 {label:1–120chars,count:positive integer,reason:1–500chars}; only for retained outcome, no unrelated PII"}},{"method":"GET","path":"/domain/historical-users/:id","auth":"exact registered domain bearer and prior historical subject association","query":{"domain":"exact registered product identity"},"description":"Resolve a retained terminal subject as id/deleted:true/name:Deleted user; no email, avatar, credentials or memberships"},{"method":"POST","path":"/auth/lifecycle-status/start","notes":"Optional diagnostics: AUTH_ACCESS_STATUS_ENABLED=true on the auth server is required. False/unset returns404 before config/client/mail/proof work, including outstanding proof redemption.","auth":"verified signed product config; rate limited","query":{"config_url":"signed product config URL"},"description":"Begin mailbox proof for read-only access reasons; generic response even for absent/deleted identity or mail failure","body":{"email":"valid email"},"response":{"ok":"true","challengeId":"opaque UUID"}},{"method":"POST","path":"/auth/lifecycle-status/verify","notes":"Optional diagnostics: AUTH_ACCESS_STATUS_ENABLED=true on the auth server is required. False/unset returns404 before config/client/mail/proof work, including outstanding proof redemption.","auth":"mailbox code and enrolled TOTP; exact config binding","query":{"config_url":"same signed product config URL"},"description":"Consume proof and return own account and authorized organisation/team access reasons; never an authentication token or internal note","body":{"challengeId":"UUID","code":"6digits","twoFactorCode":"6digits if enrolled"}},{"method":"POST","path":"/auth/debug-login/issue","description":"Mint a 30-minute single-use login code from a current source refresh family","auth":"verified config and domain bearer plus source refresh credential","query":{"config_url":"signed product config URL"},"body":{"refresh_token":"current source refresh token","previous_token":"optional previous code to invalidate atomically"},"response":{"token":"random opaque one-use code","expires_in":"1800"}},{"method":"POST","path":"/auth/debug-login/redeem","description":"Consume one code and mint an independent session after current authority checks","auth":"verified config and domain bearer","query":{"config_url":"same product config identity"},"body":{"token":"one-use login code"},"response":{"access_token":"new token","refresh_token":"new independent family","expires_in":"access lifetime seconds","refresh_token_expires_in":"remaining source lifetime seconds","token_type":"Bearer"}},{"method":"GET","path":"/internal/admin/native-apps","auth":"Admin superuser bearer","description":"List operator-owned public native app profiles","response":{"items":"Array of app profiles; no credentials"}},{"method":"POST","path":"/internal/admin/native-apps","auth":"Admin superuser bearer","description":"Register a native app; immutable reverse-domain identifier","body":{"identifier":"reverse-domain string","name":"string","enabled":"boolean","redirect_uris":"string[]","scopes":"account scope[]","methods":"[google and/or email_password]","allow_registration":"boolean","primary_color":"#RRGGBB","background_color":"#RRGGBB","text_color":"#RRGGBB"}},{"method":"PUT","path":"/internal/admin/native-apps/:id","auth":"Admin superuser bearer","description":"Replace mutable profile fields; security edits invalidate existing registrations. Identifier cannot change."},{"method":"PUT","path":"/internal/admin/native-apps/:id/icon","auth":"Admin superuser bearer","description":"Upload a bounded raster icon or remove it","body":{"image":"base64 PNG/JPEG/WebP (<=256 KB decoded), or null"}},{"method":"GET","path":"/oauth/apps/:identifier/icon","auth":"Public when app and public OAuth profile enabled","description":"Serve stored raster icon; fixed MIME and nosniff"},{"method":"GET","path":"/oauth/social/google","auth":"Public profile with registered native app and mandatory S256","description":"Start Google authentication using stored app policy","query":{"client_id":"public client","redirect_uri":"exact registered URI","code_challenge":"S256 challenge","code_challenge_method":"S256","state":"opaque state","scope":"requested scope","resource":"optional resource"}},{"method":"GET","path":"/oauth/social/complete","auth":"Secure HttpOnly public completion cookie","description":"Render hosted completion/second-factor screen"},{"method":"POST","path":"/oauth/social/complete","auth":"Same-origin JSON plus Secure HttpOnly completion cookie","description":"Complete current second-factor and signature policy, then issue public PKCE code","body":{"code":"optional six digit TOTP","setup_token":"optional enrollment token"}},{"method":"GET","path":"/","description":"Holding page linking to Admin, /llm, and /api"},{"method":"GET","path":"/api","description":"API information and full endpoint schema"},{"method":"GET","path":"/llm","description":"Markdown integration guide for LLMs and humans; links /api for JSON schema"},{"method":"GET","path":"/.well-known/jwks.json","description":"Public JWKS used to verify RS256 config JWT signatures","auth":"public","response":{"keys":"array — public RSA JWKs only; private key members are rejected at boot-time use"}},{"method":"GET","path":"/health","description":"Health check"},{"method":"GET","path":"/admin/*","description":"First-party UOA Admin CSR app served from the API origin","response":{"200":"Admin HTML shell or static asset"}},{"method":"POST","path":"/config/validate","description":"Production-safe configuration validation for raw config JSON, signed config JWTs, or config_url fetch targets","auth":"Public, IP rate limited; uses this deployment CONFIG_JWKS_URL for config JWT signature verification","body":{"config?":"object — raw config payload to schema-validate directly","config_jwt?":"string — signed config JWT to decode and validate","config_url?":"string — HTTPS URL that should return the signed config JWT"},"response":{"ok":"boolean — true when every executed validation and runtime-policy check passed","schema_valid":"boolean — true when the config payload matches the schema","jwt_signature_valid":"boolean|null — null when signature validation was skipped","domain_match":"boolean|null — null when config_url/domain matching was not applicable","checks":"object — stage-by-stage results for source, fetch, decode, secret_scan, signature, schema, runtime_policy, and domain_match","issues":"array — explicit validation/debug issues with stage, code, summary, and details","recommendations":"array — required next steps, operational notes, and optional customization guidance"}},{"method":"POST","path":"/config/verify","description":"Non-production DEBUG_ENABLED-only configuration validation for raw config JSON, config JWTs, or config_url fetch targets","auth":"Available only when DEBUG_ENABLED=true and NODE_ENV is not production; IP rate limited","body":{"config?":"object — raw config payload to schema-validate directly","config_jwt?":"string — signed config JWT to decode and validate","config_url?":"string — HTTPS URL that should return the signed config JWT","jwks_url?":"string — JWKS URL used to verify the RS256 JWT signature; defaults to CONFIG_JWKS_URL"},"response":{"ok":"boolean — true when every executed validation check passed","schema_valid":"boolean — true when the config payload matches the schema","jwt_signature_valid":"boolean|null — null when signature validation was skipped","domain_match":"boolean|null — null when config_url/domain matching was not applicable","checks":"object — stage-by-stage results for source, fetch, decode, signature, schema, and domain_match","issues":"array — explicit validation/debug issues with stage, code, summary, and details"}},{"method":"POST","path":"/auth/job-grants","description":"Create or recover one finite SalesNerd research-job authority from a fresh signed subject assertion.","auth":"verified config_url and the calling product’s existing domain-hash bearer","query":{"config_url":"signed app config whose domain matches the authenticated bearer"},"body":{"request_id":"UUID durably generated by the product before first issuance; exact retries recover the same UOA grant.","subject_token":"Fresh source-config RS256 assertion, max 60 seconds, aud=PUBLIC_BASE_URL+/auth/job-grants, with active org/team, sub/tv/jti and signed job_authorization tuple.","subject_token_type":"urn:ietf:params:oauth:token-type:jwt"},"response":{"grant_handle":"UOA-generated opaque UUID; useful only with the same authenticated product domain credential.","expires_at":"Exact signed authorized_until as RFC3339; never sliding or extended on replay.","[status]":"201 first commit; 200 exact response-loss recovery."},"notes":"Only SalesNerd ai.invoke to the exact Ledger resource is supported. The signed job_authorization claim binds request_id, parent job_id, product, resource, purpose=research_job and authorized_until (integer epoch seconds, at most 7 days). A new request ID can create another immutable grant generation only for the same original client/subject/org/team after the prior generation is expired, revoked or credential-epoch stale; an active grant in the same epoch is reused."},{"method":"POST","path":"/auth/job-grants/:grantHandle/renew","description":"Renew one short-lived, exact-resource AI invocation token for an active originally authorized SalesNerd job.","auth":"verified config_url and the calling product’s existing domain-hash bearer","query":{"config_url":"same signed app config and domain credential used to issue the grant"},"body":{"request_id":"original grant request UUID","job_id":"exact admitted parent research_job ID"},"response":{"access_token":"UOA RS256 token for https://ledger.unlikeotherai.com, product salesnerd, exact ai.invoke and original active team; max 120 seconds and never beyond grant expiry.","token_type":"Bearer","expires_in":"seconds","scope":"ai.invoke"},"notes":"Every renewal rechecks current product mapping, source role, active identity and team membership, credential epoch, grant expiry and revocation. The token carries the signed parent job grant identity; child Ledger/Water lineage remains separately bound."},{"method":"POST","path":"/auth/job-grants/:grantHandle/revoke","description":"Revoke one exact SalesNerd research-job grant; exact repeats are idempotent.","auth":"verified config_url and the calling product’s existing domain-hash bearer","query":{"config_url":"same signed app config and domain credential used to issue the grant"},"body":{"request_id":"original grant request UUID","job_id":"exact admitted parent research_job ID"},"response":{"revoked":"true"},"notes":"Revoke serializes with renew and does not require current human membership, so a cancelled job can be closed after the owner loses access. Grant identifiers alone never authenticate a request."},{"method":"POST","auth":"Domain hash bearer + verified config_url + exactly one user access token or RS256 X-UOA-Subject-Assertion (audience /org). No backend-only mode.","query":{"domain":"string (required)","config_url":"HTTPS signed config URL (required)"},"notes":"Server-to-server only. Never expose domain credentials to a browser. A session identifies the person; it does not satisfy fresh verification.","path":"/auth/action-verification/start","description":"Email a fresh single-use code for an exact product action. 20 requests per person per 15 minutes.","body":{"actionDigest":"64 lowercase hex SHA-256 binding to person, action nonce and immutable terms","description":"string (required, 1–240 chars), human-readable action for the email"},"response":{"challengeId":"UUID","expiresAt":"ISO timestamp, five minutes","twoFactorRequired":"boolean"}},{"method":"POST","auth":"Domain hash bearer + verified config_url + exactly one user access token or RS256 X-UOA-Subject-Assertion (audience /org). No backend-only mode.","query":{"domain":"string (required)","config_url":"HTTPS signed config URL (required)"},"notes":"Server-to-server only. Never expose domain credentials to a browser. A session identifies the person; it does not satisfy fresh verification.","path":"/auth/action-verification/verify","description":"Verify and consume the exact challenge. Five guesses, actor/domain/action/credential-epoch bound; replay is rejected.","body":{"actionDigest":"same exact digest","challengeId":"UUID","code":"six-digit email code","twoFactorCode":"six-digit fresh authenticator code, required for enrolled accounts"},"response":{"verified":"true","actionDigest":"verified action binding"}},{"method":"POST","path":"/auth/session-broker/validate","description":"Validate a direct Coder-to-Selkie short-lived session:broker capability against current UOA authority.","auth":"Selkie verified config_url and domain hash bearer","query":{"config_url":"signed Selkie configuration URL"},"body":{"token":"UOA RS256 resource capability (max 16384 characters)"},"response":{"sub":"stable UOA subject","expires_at":"RFC3339 expiry","active":"object {orgId,teamId}: exact UOA organisation and team ids"}},{"method":"GET","path":"/auth","description":"OAuth entrypoint — renders the auth UI","query":{"state":"string (optional, ≤2048) — opaque relying-party CSRF value, forwarded to the auth UI and replayed on the call that begins the flow. Bound to that login and echoed beside `code` on the final redirect. PKCE protects the code exchange; `state` protects the redirect binding, and neither substitutes for the other.","config_url":"string (required) — HTTPS URL to fetch signed config JWT","redirect_url":"string (optional) — OAuth redirect URL override (redirect_uri also accepted)","code_challenge":"string (required for sign-in actions) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" when code_challenge is sent","team_hint":"string (optional, ≤256 chars, id/slug-safe charset) — chooser preselect / one-click team switch (design §11.4): when the team chooser renders, a team already in the verified user's own chooser payload matching this teamId or slug is auto-selected, same as the single-team auto-skip. Client-side ONLY — an invalid or non-matching value is silently ignored (chooser renders normally) and select-team's server-side product-policy + exact ACTIVE-membership check remains the sole authority; it can never select a team the user doesn't already have.","prompt":"\"select_account\" (optional) — the person is adding another account, so a social button forwards it to /auth/social/:provider and the provider shows its account chooser instead of reusing the browser's provider session (Google). Any other value is ignored here. Selects a provider screen only; grants nothing."}},{"method":"POST","path":"/auth/login","description":"Email/password login. team_selection=\"auto\" may return the chooser; \"off\" suppresses it. A server-recognized product still resolves its mandatory exact team before 2FA, applies that Organisation policy strongest-wins, and fails closed on ambiguous placement.","auth":"config_url query param","query":{"state":"string (optional, ≤2048) — opaque relying-party CSRF value. UOA does not interpret it; it is bound to this login (login_token, 2FA bridge, social state) and echoed verbatim beside `code` on the final redirect, and beside `error` on a failed social callback. Later hops must not re-supply it: a hop presenting a different value is refused.","redirect_url":"string (optional, redirect_uri also accepted)","code_challenge":"string (required) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" (required)","request_access":"string (optional) — when truthy, auto-grant or create a pending access request"},"body":{"email":"string (required)","password":"string (required)","remember_me":"boolean (optional) — defaults to session.remember_me_default from config"},"response":{"ok":"true","kind":"\"twofa_enroll_required\" when mandatory 2FA setup must complete before code grant","code":"authorization code","redirect_to":"full redirect URL with code","twofa_required":"true (only if 2FA needed)","twofa_token":"challenge token (only if 2FA needed)","twofa_enroll_required":"true (only if effective policy is required and user is not enrolled)","setup_token":"short-lived 2FA setup token (only with twofa_enroll_required)","otpauth_uri":"otpauth:// URI (only with twofa_enroll_required)","qr_svg":"self-contained data:image/svg+xml;base64 QR with logo (only with twofa_enroll_required)","manual_secret":"manual-entry TOTP secret text (only with twofa_enroll_required)","access_request_status":"\"pending\" when request_access created a pending access request"}},{"method":"POST","path":"/auth/start","description":"Slack-style email-first entry point — superset of /auth/register. Always sends the existing magic-link instructions; additionally issues a 6-digit sign-in code when config.login_flow.email_code_enabled is true. Response is always the same generic message (no enumeration).","auth":"config_url query param","query":{"state":"string (optional, ≤2048) — opaque relying-party CSRF value. UOA does not interpret it; it is bound to this login (login_token, 2FA bridge, social state) and echoed verbatim beside `code` on the final redirect, and beside `error` on a failed social callback. Later hops must not re-supply it: a hop presenting a different value is refused.","redirect_url":"string (optional, redirect_uri also accepted)","code_challenge":"string (required) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" (required)","request_access":"string (optional) — when truthy, auto-grant or create a pending access request"},"body":{"email":"string (required)"},"response":{"message":"\"We sent instructions to your email\" (always, regardless of email code issuance)","code":"\"EMAIL_ALREADY_REGISTERED\" with HTTP 409 only when existing_user_registration_behavior=\"inline_sign_in\""}},{"method":"POST","path":"/auth/verify-code","description":"Verify a 6-digit sign-in code issued by /auth/start (requires config.login_flow.email_code_enabled). 5 wrong attempts kill the code; every failure mode (no code, wrong code, expired, dead) returns the same generic auth error. IP + email rate-limited. With team_selection=\"off\", recognized products suppress the chooser but resolve and bind their exact team before 2FA.","auth":"config_url query param","query":{"state":"string (optional, ≤2048) — opaque relying-party CSRF value. UOA does not interpret it; it is bound to this login (login_token, 2FA bridge, social state) and echoed verbatim beside `code` on the final redirect, and beside `error` on a failed social callback. Later hops must not re-supply it: a hop presenting a different value is refused.","redirect_url":"string (optional, redirect_uri also accepted)","code_challenge":"string (required) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" (required)","request_access":"string (optional) — when truthy, auto-grant or create a pending access request"},"body":{"email":"string (required)","code":"string (required) — 6-digit sign-in code","remember_me":"boolean (optional) — defaults to session.remember_me_default from config"},"response":{"login_token?":"short-lived, one-time chooser capability (only when config.login_flow.team_selection=\"auto\") — binds this verified user/domain to the exact config URL + parsed-config fingerprint, redirect, PKCE, remember-me, request-access, expiry, and JTI; authorizes no other continuation","teams?":"array of { teamId, orgId, name, slug, role, iconUrl, avatarImageUrl, orgName, orgSlug } — this user's ACTIVE team memberships on this domain (only with login_token). avatarImageUrl is the public /teams/:teamId/avatar form (never null, no credential needed) — the chooser renders in a popup that holds no bearer","pending_invites?":"array of { inviteId, teamName, orgName, invitedBy } — pending invites for this email on this domain (only with login_token). orgName is the inviting organisation (null only for a legacy row without it): two organisations can each own a team called \"General\", and the invite card carries no other organisation label. invitedBy is the inviter NAME and null when no name is known — never an e-mail address","can_create_org?":"boolean (only with login_token)","creatable_orgs?":"array of { orgId, orgName } — organisations this user may add a team to via POST /auth/create-team (ACTIVE owner/admin there + org_features.allow_user_create_team). Empty unless the domain opted in. Distinct from can_create_org, which permits creating a new organisation","ok":"true (when team_selection is \"off\" — finalizes immediately like /auth/login)","code":"authorization code (team_selection \"off\" branch only)","redirect_to":"full redirect URL with code (team_selection \"off\" branch only)","twofa_required":"true (only if 2FA needed, team_selection \"off\" branch only)","twofa_token":"challenge token (only if 2FA needed, team_selection \"off\" branch only)"}},{"method":"POST","path":"/auth/select-team","description":"Choose a team (or accept/decline a pending invite, or redeem a shareable invite link) using the login_token bridge from a verified identity path. Rejects generically if the signature/expiry/config URL/semantic config fingerprint differs, if any caller-supplied redirect/PKCE/remember-me/request-access field retargets the signed continuation, if a selected/accepted exact org + team scope is not ACTIVE, or if an invite link is invalid or cannot produce an ACTIVE exact scope. Final selection claims the hashed JTI first in the transaction, before invite/audit/access-request-email effects; a concurrent replay cannot perform those effects, while later failure rolls the claim back for retry. Choices and decline are non-consuming. Scope is revalidated after 2FA/signatures immediately before code issuance and again inside the single code-consume/token-creation transaction at exchange. The chooser-selection and code-exchange transactions hold the ordered membership locks shared with lifecycle writers; post-2FA/signature code issuance revalidates but does not hold those locks.","auth":"config_url query param + login_token body field","query":{"redirect_url":"string (optional, redirect_uri also accepted)","code_challenge":"string (required) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" (required)","request_access":"string (optional) — when truthy, auto-grant or create a pending access request"},"body":{"login_token":"string (required) — bridge token from /auth/verify-code or /auth/login","teamId?":"string (optional) — an ACTIVE team membership to select","inviteId?":"string (optional) — a pending invite id (accept or decline via `action`)","inviteLinkToken?":"string (optional) — a shareable team invite-link token (from GET /auth/team-invite-link/:token). Mutually exclusive with teamId/inviteId; redeems the link and finalizes scoped to its team, only now that identity is verified — an invite link never grants membership on its own.","action?":"\"accept\" | \"decline\" (optional) — default \"accept\" when inviteId is present","remember_me":"boolean (optional) — when present must equal the value signed at identity verification; omission uses that signed value"},"response":{"ok":"true","code":"authorization code, now carrying the selected team scope (active claim on exchange) when a team/invite was resolved","redirect_to":"full redirect URL with code","twofa_required":"true (only if the selected org requires 2FA)","twofa_token":"challenge token (only if 2FA needed)","twofa_enroll_required":"true (only if the selected org requires 2FA and the user is not enrolled)","login_token?":"refreshed bridge token + chooser payload (decline-invite response only)","access_request_status":"\"pending\" when request_access created a pending access request"}},{"method":"POST","path":"/auth/create-organisation","description":"Create and immediately select a verified user's first team from the hosted SSO chooser. The login_token bridge and all supplied continuation fields must match the signed identity-verification continuation. Within one transaction, UOA claims the bridge token, creates the organisation and its default team with the requested hosted-chooser visibility, validates the exact ACTIVE client team scope, and completes required 2FA/code issuance. The organisation slug is the canonical tenant DNS label: it is exposed in the resulting access token as active.tenantSlug and is unique for the client domain. Team slugs remain local to their organisation and must not be used as tenant subdomains.","auth":"config_url query param + login_token body field","query":{"redirect_url":"string (optional, redirect_uri also accepted)","code_challenge":"string (required) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" (required)","request_access":"string (optional) — when truthy, auto-grant or create a pending access request"},"body":{"login_token":"string (required) — bridge token from /auth/verify-code or /auth/login","name":"string (required, 1–100 characters after trimming) — team name; UOA derives the organisation/tenant slug","join_policy":"\"HIDDEN\" | \"INVITE_ONLY\" | \"OPEN_TO_ORG\" (optional; defaults to INVITE_ONLY) — default-team visibility. HIDDEN is private/discoverable only by invitees; OPEN_TO_ORG lets active organisation members join themselves","remember_me":"boolean (optional) — when present must equal the value signed at identity verification; omission uses that signed value"},"response":{"ok":"true","code":"authorization code carrying the new selected team scope when issuance completes","redirect_to":"full redirect URL with code","twofa_required":"true when the new selected org requires enrolled 2FA","twofa_token":"challenge token (only if 2FA needed)","twofa_enroll_required":"true when the new selected org requires 2FA and the user is not enrolled","access_request_status":"\"pending\" when request_access created a pending access request"}},{"method":"POST","path":"/auth/create-team","description":"Create and immediately select a FURTHER team (team) inside an organisation the verified user already belongs to, from the hosted SSO chooser. The sibling of /auth/create-organisation, which creates a new organisation: an organisation is the level above a team, so this route writes into an existing tenant and is authorized accordingly — the acting user must be an ACTIVE owner/admin of org_id (the same rule POST /org/organisations/:orgId/teams enforces), the organisation must belong to this config's domain, and the domain must set org_features.allow_user_create_team. The org's max_teams_per_org cap still applies. Same transaction envelope as /auth/create-organisation: the bridge token is claimed alongside the new team with its requested hosted-chooser visibility, the creator's ACTIVE team membership, the team-scope validation, and required 2FA/code issuance. The chooser's creatable_orgs lists exactly the organisations this will accept.","auth":"config_url query param + login_token body field","query":{"redirect_url":"string (optional, redirect_uri also accepted)","code_challenge":"string (required) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" (required)","request_access":"string (optional) — when truthy, auto-grant or create a pending access request"},"body":{"login_token":"string (required) — bridge token from /auth/verify-code or /auth/login","org_id":"string (required) — an organisation from the chooser's creatable_orgs; anything else is the generic auth failure","name":"string (required, 1–100 characters after trimming) — team name; UOA derives the team slug, local to the organisation","join_policy":"\"HIDDEN\" | \"INVITE_ONLY\" | \"OPEN_TO_ORG\" (optional; defaults to INVITE_ONLY) — team visibility. HIDDEN is private/discoverable only by invitees; OPEN_TO_ORG lets active organisation members join themselves","remember_me":"boolean (optional) — when present must equal the value signed at identity verification; omission uses that signed value"},"response":{"ok":"true","code":"authorization code carrying the new selected team scope when issuance completes","redirect_to":"full redirect URL with code","twofa_required":"true when the org requires enrolled 2FA","twofa_token":"challenge token (only if 2FA needed)","twofa_enroll_required":"true when the org requires 2FA and the user is not enrolled","access_request_status":"\"pending\" when request_access created a pending access request"}},{"method":"POST","path":"/auth/session-choices","description":"Hydrate the team-chooser payload for a login_token bridge seeded via a redirect, since a GET redirect cannot inline chooser JSON. Verifies signature, expiry, exact config URL, and the canonical fingerprint of the currently verified parsed config. This read is deliberately non-consuming; only final selection claims the hashed JTI. Introduces no enumeration because it answers only for an already-verified capability.","auth":"config_url query param + login_token body field","query":{"config_url":"string (required)"},"body":{"login_token":"string (required) — bridge token from the redirecting flow (e.g. the social callback)"},"response":{"teams":"array of { teamId, orgId, name, slug, role, iconUrl, avatarImageUrl, orgName, orgSlug } — this user's ACTIVE team memberships on this domain. avatarImageUrl is the public /teams/:teamId/avatar form (never null, no credential needed)","pending_invites":"array of { inviteId, teamName, orgName, invitedBy } — pending invites for this email on this domain. orgName is the inviting organisation (null only for a legacy row without it); invitedBy is the inviter NAME, never an e-mail address, and null when no name is known","can_create_org":"boolean","creatable_orgs":"array of { orgId, orgName } — organisations this user may add a team to via POST /auth/create-team"}},{"method":"POST","path":"/auth/register","description":"User registration — sends verification email, or an inline already-registered response when the signed config opts in","auth":"config_url query param","query":{"state":"string (optional, ≤2048) — opaque relying-party CSRF value. UOA does not interpret it; it is bound to this login (login_token, 2FA bridge, social state) and echoed verbatim beside `code` on the final redirect, and beside `error` on a failed social callback. Later hops must not re-supply it: a hop presenting a different value is refused.","redirect_url":"string (optional)","request_access":"string (optional) — bypass gating and request configured-team access","code_challenge":"string (required) — exactly 43-char PKCE S256 challenge preserved through email verification","code_challenge_method":"\"S256\" (required)"},"body":{"email":"string (required)"},"response":{"message":"\"We sent instructions to your email\" (default, no enumeration)","code":"\"EMAIL_ALREADY_REGISTERED\" with HTTP 409 only when existing_user_registration_behavior=\"inline_sign_in\""}},{"method":"POST","path":"/auth/verify-email","description":"Complete email verification (registration). For a non-invite token with config.login_flow.team_selection=\"auto\", 2+ ACTIVE teams, a pending invite, or zero teams with can_create_org return the team chooser. Exactly one ACTIVE team/no invite is selected server-side and carried through applicable 2FA into the authorization code and active claim. An invite-bound token is already an explicit selection: its accepted orgId/teamId bypasses the chooser, still enforces 2FA, and is preserved through code/access/refresh rotation. A direct email invitation may omit PKCE: it accepts only its live token and returns invite_accepted without issuing an OAuth code. team_selection=\"off\" leaves legacy clients unscoped, but a recognized product pre-binds one exact team before 2FA without showing the chooser.","auth":"config_url query param","query":{"state":"string (optional, ≤2048) — opaque relying-party CSRF value. UOA does not interpret it; it is bound to this login (login_token, 2FA bridge, social state) and echoed verbatim beside `code` on the final redirect, and beside `error` on a failed social callback. Later hops must not re-supply it: a hop presenting a different value is refused.","redirect_url":"string (optional)","code_challenge":"string (required except a direct team invitation) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" (required whenever code_challenge is sent)","request_access":"string (optional) — auto-grant or create a pending access request"},"body":{"token":"string (required) — email verification token","password":"string (optional) — required for password_required registration mode","name":"string (optional, ≤120, trimmed) — self-declared display name. Stored only while the account has no name yet, so it never overwrites an established one; for an invitation the inviter-supplied name remains the fallback when this is omitted."},"response":{"ok":"true (team_selection \"off\"/skipped branch only — finalizes immediately)","invite_accepted":"true only for a direct live team invitation without PKCE; no authorization code is issued","code":"authorization code (team_selection \"off\"/skipped branch only)","redirect_to":"redirect URL (team_selection \"off\"/skipped branch only)","access_request_status":"\"pending\" when request_access created a pending access request","twofa_required":"true when the selected/accepted team requires enrolled 2FA","twofa_token":"team-bearing challenge token when enrolled 2FA must complete before code issuance","twofa_enroll_required":"true when effective policy requires enrollment before the scoped code can be issued","setup_token":"team-bearing setup token when twofa_enroll_required is true (alongside QR/manual setup fields)","login_token?":"short-lived bridge JWT (only when the chooser gate passes) — authorizes ONLY POST /auth/select-team for this verified user","teams?":"array of { teamId, orgId, name, slug, role, iconUrl, avatarImageUrl, orgName, orgSlug } (only with login_token). avatarImageUrl is the public /teams/:teamId/avatar form (never null, no credential needed); orgName is the owning organisation, so same-named teams in different orgs stay distinguishable","pending_invites?":"array of { inviteId, teamName, orgName, invitedBy } (only with login_token); orgName is the inviting organisation","can_create_org?":"boolean (only with login_token)","creatable_orgs?":"array of { orgId, orgName } — organisations this user may add a team to via POST /auth/create-team (ACTIVE owner/admin there + org_features.allow_user_create_team). Empty unless the domain opted in. Distinct from can_create_org, which permits creating a new organisation"}},{"method":"POST","path":"/auth/token","description":"Exchange an authorization code or refresh token for the legacy access + refresh pair, explicitly move that pair to one exact authorized team, or exchange a source-signed JWT assertion / UOA-issued audience-bound access token for a resource-bound confidential access token. Authorization-code exchange re-resolves current exact-team 2FA policy and user enrollment and rejects a code lacking required interactive TOTP proof before token-family creation.","auth":"config_url query param + domain hash bearer token","body":{"grant_type?":"\"authorization_code\" (default), \"refresh_token\", \"urn:unlikeotherai:params:oauth:grant-type:team-switch\", or \"urn:ietf:params:oauth:grant-type:token-exchange\"","code?":"authorization code (for authorization_code grant)","redirect_url?":"required for authorization_code grant; must match issued URL","code_verifier?":"required for authorization_code grant; must match the S256 challenge","refresh_token?":"refresh token (for refresh_token and team-switch grants)","organization_id?":"exact target organisation id (required for team-switch grant; must be paired with team_id)","team_id?":"exact target team id (required for team-switch grant; must belong to organization_id)","subject_token?":"For a first hop: one-time RS256 JWT with exp-iat <= 60 seconds, signed by the source config JWKS. For a chained hop: UOA-issued RS256 at+jwt access token with aud exactly https://<authenticated caller config domain>, non-null org/active, and remaining lifetime.","subject_token_type?":"\"urn:ietf:params:oauth:token-type:jwt\" for a first-hop assertion, or \"urn:ietf:params:oauth:token-type:access_token\" for a chained UOA token","product?":"lowercase product identifier (required for token-exchange grant); must match the DB mapping bound to the authenticated app domain credential","resource?":"exact DB-allowlisted HTTPS resource URI (required for token-exchange grant; becomes access-token aud)","scope?":"space-delimited exact requested scopes (required for token-exchange grant); supported values are ai.invoke, billing.read, token.provision, memory.read, and memory.write, and every requested scope must be allowed by the product mapping"},"response":{"access_token":"Authorization-code/refresh/team-switch grants: legacy HS256 JWT with aud=\"uoa:access-token\". Confidential token-exchange grant: at-most-5-minute RS256 JWT bound to resource, verifiable at GET /oauth/jwks.json, with product, exact requested scope, stable sub, validated provenance, and no domain bearer credential. A chained result never outlives its inbound token.","access_token.active.tenantSlug?":"canonical organisation slug for the selected team and safe DNS tenant label. Team.slug is not a tenant DNS key because it is only unique within its organisation.","expires_in":"number — seconds until access_token expiry","refresh_token?":"string — opaque, server-side only; authorization-code/refresh/team-switch grants only, never hand to the browser","refresh_token_expires_in?":"number — seconds until refresh_token expiry; authorization-code/refresh/team-switch grants only","issued_token_type?":"\"urn:ietf:params:oauth:token-type:access_token\"; confidential token-exchange grant only","scope?":"exact granted request subset of \"ai.invoke\", \"billing.read\", \"token.provision\", \"memory.read\", and/or \"memory.write\"; token.provision is never implied by ai.invoke; confidential token-exchange grant only","token_type":"\"Bearer\"","firstLogin?":"object { memberships: { orgs, teams }, pending_invites, capabilities { can_create_org, can_accept_invite } } — included only on authorization_code exchange when org_features.enabled is true. Never included on refresh_token or team-switch grants.","[note]":"There is NO top-level `user` field. User identity lives inside access_token claims (read claims.sub). Every immediate caller uses its own app credential and enabled DB mapping; no shared/cross-app/fallback key or webhook secret is accepted.","[rate limit]":"Authorization-code, refresh, and team-switch grants: 10/min per IP, plus a service-wide global ceiling of 30,000/min per instance. Confidential exchange: 600/min per authenticated source domain plus 60/min per verified source-domain user.","401 refresh policy":"If the domain signature policy changed and the refresh-token user is incomplete, or a stored scoped session no longer has its exact active product mapping plus ACTIVE org/team memberships, the valid refresh token is not rotated or consumed. Restart interactive authorization; UOA never silently changes the team.","team-switch contract":"The custom grant requires refresh_token + organization_id + team_id and no operation id. It checks the exact target ACTIVE memberships, current product policy, current target 2FA policy, and the family's immutable authorization-code 2FA assurance before rotating once to that exact scope. An ordinary refresh always preserves scope. Same-scope requests are rejected without consumption.","team-switch errors":"Before an edge commits, 403 TEAM_NOT_AVAILABLE means the exact pair is unavailable or policy denies it and 403 INTERACTION_REQUIRED means fresh interactive assurance is needed; both leave the source live. 409 TEAM_SWITCH_CONFLICT means the deterministic family already advanced under another grant or target and does not revoke that valid family. If an exact response-loss retry finds its already-committed target no longer policy-valid, UOA retires only that family and returns authenticated 401 INVALID_REFRESH_TOKEN. Invalid/post-grace reuse also returns that stable code while keeping the underlying reason opaque; client-authentication failures remain generic.","subject verification unavailable":"503 INTEGRATION_JWKS_UNAVAILABLE means the source public keys could not be fetched because of a network, transient DNS (EAI_AGAIN), timeout, rate-limit or upstream-server failure. No exchange occurs; retry with backoff using a fresh assertion without ending the user session. A real invalid signature, missing key, expired assertion or unsafe JWKS destination remains a 401 refusal.","confidential exchange errors":"A 403 carrying the public code TOKEN_EXCHANGE_SUBJECT_FORBIDDEN means the person’s own current state refuses the exchange (moved credential epoch, unknown user, lost source-domain role, or unavailable selected team) without saying which; ask them to sign in again. A 403 with the generic body and no code is the calling product’s own mapping, scope, or team-context configuration, decided before any subject lookup.","refresh response-loss recovery":"For 120 seconds after rotation, retrying the same predecessor with the same authenticated app credential, grant intent, exact team target, and client context returns the verified current successor. Every descendant must retain that operation's expected scope; a later scope transition produces TEAM_SWITCH_CONFLICT instead of returning changed scope. Outside the window, any predecessor use revokes the family and prior access-token version."}},{"method":"POST","path":"/auth/revoke","description":"Revoke one refresh family, optionally preserving independent sessions","auth":"config_url query param + domain hash bearer token","body":{"refresh_token":"string (required)","scope":"optional family; omission retains global credential revocation"},"response":{"ok":"true"}},{"method":"POST","path":"/auth/reset-password/request","description":"Initiate password reset (no enumeration)","auth":"config_url query param","body":{"email":"string (required)"},"response":{"message":"\"We sent instructions to your email\" (always)"}},{"method":"POST","path":"/auth/reset-password","description":"Complete password reset with token","auth":"config_url query param","body":{"token":"string (required)","password":"string (required)"},"response":{"ok":"true"}},{"method":"GET","path":"/auth/email/reset-password","description":"Email link landing — renders set-password UI","query":{"token":"string (required)","config_url":"string (required)"}},{"method":"GET","path":"/auth/email/twofa-reset","description":"Email link landing for 2FA reset — renders confirmation page only","query":{"token":"string (required)","config_url":"string (required)"}},{"method":"POST","path":"/auth/email/twofa-reset/confirm","description":"Confirm email-based 2FA reset and consume the one-time token","auth":"config_url query param","query":{"token":"string (required)","config_url":"string (required)"}},{"method":"GET","path":"/auth/email/link","description":"Email registration/login link landing. A direct password invitation without PKCE opens account creation with the invitation email fixed in a read-only field; the invitation token, not that field, remains authoritative. The account-creation screen also pre-fills its editable name field with the name the inviter gave (carried as invite_name, which is sent whenever the invitation has one). An invitation for an address that already has an account carries a LOGIN_LINK token and likewise arrives without PKCE: it is accepted here and answered with a terminal confirmation page, because there is no code verifier to end in an authorization code and a login restart would drop the invitation. A refused invitation acceptance answers 400 rather than showing a login form: an elapsed invite/token deadline renders \"This invitation has expired\", while revoked, used, conflicting, and every other unusable state render \"This invitation can’t be used\". For a non-invite link with config.login_flow.team_selection=\"auto\", 2+ ACTIVE teams, a pending invite, or zero teams with can_create_org redirect to the team chooser; exactly one ACTIVE team/no invite is selected server-side and carried through applicable 2FA into the code. Invite-bound links carry the accepted invite orgId/teamId through the same 2FA/code/token pipeline without showing the chooser. team_selection=\"off\" leaves legacy clients unscoped, but a recognized product pre-binds one exact team before 2FA without showing the chooser.","query":{"state":"string (optional, ≤2048) — opaque relying-party CSRF value. UOA does not interpret it; it is bound to this login (login_token, 2FA bridge, social state) and echoed verbatim beside `code` on the final redirect, and beside `error` on a failed social callback. Later hops must not re-supply it: a hop presenting a different value is refused.","token":"string (required)","config_url":"string (required)","redirect_url":"string (optional) — must be one of config.redirect_urls. On an invitation terminal page it also becomes a \"Continue to <product>\" link (product name from ui_theme.logo.alt, else the config domain) and rides the invite-registration continuation so the hosted \"You’re in\" view offers the same link. A value not in config.redirect_urls is dropped: the page renders exactly as it does without the parameter.","code_challenge":"string (optional for recovery; required to complete the one-click OAuth grant) — exactly 43-char PKCE S256 challenge preserved through email verification","code_challenge_method":"\"S256\" when code_challenge is sent","request_access":"string (optional) — preserves access-request intent through email auth"}},{"method":"GET","path":"/auth/email/team-invite","description":"Team invite landing page with accept/decline actions. Emailed invitations are valid for 24 hours. The page names the inviter when UOA knows their name. An elapsed invitation or email-token deadline renders \"This invitation has expired\"; every other unusable state renders \"This invitation can’t be used\".","query":{"token":"string (required)","config_url":"string (required)","redirect_url":"string (optional) — carried onto the Accept action so the terminal page can offer a \"Continue to <product>\" link. Only honoured when it is one of config.redirect_urls."}},{"method":"GET","path":"/auth/email/team-invite/decline","description":"Decline a team invitation","query":{"token":"string (required)","config_url":"string (required)"}},{"method":"GET","path":"/auth/email/team-invite-open/:inviteId.gif","description":"Tracking pixel for team invite open events"},{"method":"GET","path":"/auth/team-invite-link/:token","description":"Shareable team invite-link landing page. Public, IP-rate-limited, no auth. Validates the token WITHOUT redeeming it or granting any membership (unknown/revoked/expired/over-cap/HIDDEN all render the same generic invalid-link page). A valid token renders the normal Auth UI bootstrapped to start email verification, carrying invite_link_token for the client to pass into POST /auth/select-team once identity is verified.","query":{"config_url":"string (required)","redirect_url":"string (optional)"}},{"method":"GET","path":"/auth/social/:provider","description":"Initiate social OAuth flow (google, facebook, github, linkedin, apple). Sets a signed, HttpOnly `uoa_social_state` cookie (SameSite=Lax, Path=/auth) that binds the OAuth `state` to the browser; the cookie must be returned to /auth/callback. A direct team invitation may use invite_token without PKCE; its raw capability is retained only in a second signed HttpOnly cookie and never placed in provider-visible state.","query":{"state":"string (optional, ≤2048) — opaque relying-party CSRF value. UOA does not interpret it; it is bound to this login (login_token, 2FA bridge, social state) and echoed verbatim beside `code` on the final redirect, and beside `error` on a failed social callback. Later hops must not re-supply it: a hop presenting a different value is refused.","config_url":"string (required)","redirect_url":"string (optional)","code_challenge":"string (required except a direct live team invitation) — exactly 43-char PKCE S256 challenge","code_challenge_method":"\"S256\" (required whenever code_challenge is sent)","request_access":"string (optional) — routes social auth through configured-team access policy","invite_token":"string (optional) — direct team invitation capability. The provider-verified email must exactly match the invite email; otherwise authentication is rejected.","prompt":"\"select_account\" (optional) — forwarded from /auth. Google receives prompt=select_account and shows its account chooser; other providers ignore it. Any other value is refused with a generic 400."}},{"method":"GET","path":"/auth/callback/:provider","description":"OAuth provider callback. Requires the signed `uoa_social_state` cookie set at /auth/social to match the nonce embedded in `state` (login-CSRF protection); the cookie is single-use and cleared on consume. A direct invitation also requires its signed HttpOnly invite-capability cookie and an exact match between its live invitation email and the provider-verified email; it accepts the invite without issuing an OAuth code. With team_selection=\"auto\", team is resolved before 2FA: 2+ ACTIVE teams, any pending invite, or zero teams with can_create_org redirect with a login_token chooser bridge, while exactly one ACTIVE team/no invite is selected server-side and its exact orgId/teamId survives any 2FA challenge or enrollment token into the authorization code and active token claim. team_selection=\"off\" leaves legacy clients unscoped; recognized products suppress the chooser but pre-bind their exact team before 2FA."},{"method":"GET","path":"/auth/domain-mapping","description":"Look up org/team mapping for an email domain","auth":"config_url query param + domain hash bearer token","query":{"config_url":"string (required)","email_domain":"string (required)"}},{"method":"POST","path":"/2fa/setup","description":"Start or render TOTP 2FA enrollment for a user","auth":"config_url query param. Normal self-service requires X-UOA-Access-Token; forced enrollment rendering may send setup_token in the body before an auth code exists.","body":{"setup_token":"string (optional) — when present, re-renders the setup QR from the encrypted secret in the token"},"response":{"otpauth_uri":"otpauth:// URI for the authenticator app","qr_svg":"self-contained data:image/svg+xml;base64 QR with config logo embedded","setup_token":"short-lived signed JWT containing userId, encryptedSecret, domain, and configUrl","manual_secret":"base32 secret for manual authenticator entry"}},{"method":"POST","path":"/2fa/enroll","description":"Verify the initial TOTP code and enable 2FA","auth":"config_url query param + setup_token body. X-UOA-Access-Token is accepted for normal self-service binding but is not required for forced enrollment before code grant.","body":{"setup_token":"string (required) — short-lived setup token from /2fa/setup or login enforcement","code":"string (required) — current 6-digit TOTP"},"response":{"ok":"true","code":"authorization code when enroll completes a forced login","redirect_to":"redirect URL when enroll completes a forced login","access_request_status":"\"pending\" when forced login created a pending access request"}},{"method":"POST","path":"/2fa/disable","description":"Disable enrolled 2FA for the access-token user","auth":"config_url query param + X-UOA-Access-Token. Blocked when effective policy is required.","body":{"code":"string (required) — current 6-digit TOTP"},"response":{"ok":"true"}},{"method":"POST","path":"/2fa/verify","description":"Verify TOTP 2FA code during login","auth":"config_url query param","body":{"twofa_token":"string (required)","code":"string (required) — 6-digit TOTP"},"response":{"ok":"true","code":"authorization code","redirect_to":"redirect URL","access_request_status":"\"pending\" when request_access created a pending access request"}},{"method":"POST","path":"/2fa/reset/request","description":"Initiate 2FA reset via email","auth":"config_url query param","body":{"email":"string (required)"}},{"method":"POST","path":"/2fa/reset","description":"Complete 2FA reset with email token","auth":"config_url query param","body":{"token":"string (required)"},"response":{"ok":"true"}},{"method":"GET","path":"/i18n/get","description":"Fetch translation data for a language","query":{"lang":"string (required)","config_url":"string (required)"}},{"method":"POST","path":"/billing/v1/sms/numbers/begin","auth":"CUSTOMER_LIFECYCLE key and exact-endpoint fresh actor; organisation billing manager","description":"Bind one exact mobile number and accepted quote to hosted monthly subscription Checkout.","body":{"product":"registered product identifier","organisation_id":"exact UOA organisation ID","team_id":"exact active actor context team ID","user_id":"exact current UOA actor subject","resource_id":"Stable acquisition-operation ID","quote_id":"Accepted quote ID","phone_number":"Exact SMS-capable available mobile E.164"},"response":{"200":"Exact number resource, pinned final quote, payment state and hosted Checkout URL","409":"Changed binding, expired unaccepted quote or canceled resource"}},{"method":"POST","path":"/billing/v1/sms/numbers/status","auth":"Product ENTITLEMENT app key plus fresh RS256 X-UOA-Actor with exact endpoint audience; live epoch and exact membership rechecked","description":"Read exact organisation-owned number payment status with organisation manager authority.","body":{"product":"registered product identifier","organisation_id":"exact UOA organisation ID","team_id":"exact active actor context team ID","user_id":"exact current UOA actor subject","resource_id":"Stable exact resource ID"},"response":{"200":"Strict customer-safe number payment state","404":"Not visible or missing"}},{"method":"POST","path":"/billing/v1/sms/numbers/runtime-status","auth":"Product SMS_RUNTIME key","description":"Read exact product-bound number payment state for machine recovery.","body":{"product":"Registered product","resource_id":"Stable exact resource ID"},"response":{"200":"Strict customer-safe number payment state","404":"No exact product resource"}},{"method":"POST","path":"/billing/v1/sms/numbers/attach","auth":"Product SMS_RUNTIME key","description":"Attach the exact verified acquired provider number to its paid monthly resource.","body":{"product":"Registered product","resource_id":"Stable exact resource ID","account_sid":"Configured provider account","phone_number_sid":"Exact acquired PN SID"},"response":{"200":"Number state","409":"Payment inactive or provider identity mismatch"}},{"method":"POST","path":"/billing/v1/sms/numbers/end","auth":"Product SMS_RUNTIME key","description":"Fence missing acquisition or end subscription after verified provider release.","body":{"product":"Registered product","resource_id":"Stable exact resource ID","reason":"released | acquisition_unavailable"},"response":{"200":"Number state or exact missing-resource cancellation tombstone","503":"Payment/provider outcome unresolved; no false cancellation or refund"}},{"method":"POST","path":"/billing/v1/sms/quotes","auth":"Product ENTITLEMENT app key plus fresh RS256 X-UOA-Actor with exact endpoint audience; live epoch and exact membership rechecked","description":"Issue an opaque, scoped and expiring UOA final customer mobile-number or SMS quote.","body":{"product":"registered product identifier","organisation_id":"exact UOA organisation ID","team_id":"exact active actor context team ID","user_id":"exact current UOA actor subject","country":"ISO country verified against destination for outbound","number_type":"mobile","direction":"monthly | inbound | outbound","destination":"E.164 for outbound; null otherwise","carrier":"null; outbound uses conservative maximum mobile carrier rate","mcc":"null","mnc":"null"},"response":{"200":"id, exact decimal amount, currency (monthly: the organisation billing region currency, GBP, USD or EUR; messages: USD), expiry, rate_basis and exact scope only","409":"BILLING_REGION_REQUIRED: choose the organisation billing region first; BILLING_SMS_MONTHLY_PRICE_BELOW_COST: the flat monthly price does not cover the provider rental in this country","503":"Provider or accepted dated UOA currency policy unavailable"},"notes":"No customer-supplied provider costs, carrier narrowing or exchange rates. Monthly quote display expiry does not cancel accepted paid resource terms."},{"method":"POST","path":"/billing/v1/sms/quotes/verify","auth":"Product ENTITLEMENT app key plus fresh RS256 X-UOA-Actor with exact endpoint audience; live epoch and exact membership rechecked","description":"Verify the persisted exact quote binding and current expiry before customer acceptance.","body":{"product":"registered product identifier","organisation_id":"exact UOA organisation ID","team_id":"exact active actor context team ID","user_id":"exact current UOA actor subject","country":"ISO country verified against destination for outbound","number_type":"mobile","direction":"monthly | inbound | outbound","destination":"E.164 for outbound; null otherwise","carrier":"null; outbound uses conservative maximum mobile carrier rate","mcc":"null","mnc":"null","quote_id":"opaque persisted UOA quote ID"},"response":{"200":"Same strict final customer quote","409":"Expired or mismatched quote scope"}},{"method":"GET","path":"/internal/admin/billing/ledger-runtime-keys","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","description":"List product-bound Ledger runtime-key metadata; never plaintext or digests.","response":{"200":"{ keys: [{ id, product, key_prefix, ledger_audience, source_domain, created_at, revoked_at }] }; no-store"}},{"method":"POST","path":"/internal/admin/billing/ledger-runtime-keys","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","description":"Issue one Ledger runtime key bound to an active product, exact HTTPS audience and original source domain; plaintext is returned once.","body":{"product":"active billing-service identifier","ledger_audience":"exact HTTPS Ledger origin","source_domain":"original product config-domain hostname"},"response":{"200":"{ id, key_prefix, created_at, secret }; no-store; secret cannot be recovered"}},{"method":"POST","path":"/internal/admin/billing/ledger-runtime-keys/:keyId/revoke","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","description":"Revoke the exact Ledger runtime key; irreversible and audited.","response":{"200":"{ id, revoked_at }; no-store"}},{"method":"POST","path":"/billing/v1/ledger/reservations/:dispatchId/exception","description":"Record one selected immutable over-bound paid receipt as a held operator exception, without releasing the financial reservation.","auth":"Existing exact-product Ledger RuntimeKey bearer","body":{"receipt_id":"selected immutable Ledger receipt ID","raw_cost_actual":"observed provider cost decimal, maximum 18 places","currency":"USD","evidence_digest":"SHA-256 of frozen dispatch/receipt/cost/bound/context tuple","source":"ledger_selected_provider_receipt"},"response":{"200":"HELD_OPERATOR_RECONCILIATION or idempotently replayed WRITTEN_OFF decision","409":"Missing frozen bound, mismatched evidence, stale/rebound receipt or ineligible dispatch"}},{"method":"GET","path":"/billing/v1/ledger/reservations/:dispatchId/exception","description":"Read the exact operator decision for one product-bound Ledger dispatch before Ledger marks it settled.","auth":"Original exact-product Ledger RuntimeKey bearer","response":{"200":"HELD_OPERATOR_RECONCILIATION or WRITTEN_OFF with gross, collectible and waived microcredits","404":"No exception for this product-bound dispatch"}},{"method":"GET","path":"/internal/admin/billing/paid-usage-exceptions","description":"Show the oldest 100 held selected receipts with frozen bounds and maximum original credit holds for platform reconciliation.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","response":{"200":"{ exceptions: private held receipt facts[], has_more }"}},{"method":"POST","path":"/internal/admin/billing/paid-usage-exceptions/:dispatchId/write-off","description":"A recently authenticated platform superuser caps customer collection at the original hold, books gross rated usage and explicitly waives the excess once.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row; access token iat within five minutes, live user epoch and role rechecked under row locks","body":{"evidence_digest":"exact held Ledger evidence SHA-256","idempotency_key":"lowercase 64-hex key stable across retries","reason":"operator explanation, 12–500 characters"},"response":{"200":"WRITTEN_OFF with gross_rated_microcredits, collectible_microcredits and waived_microcredits","409":"Conflicting receipt, replay key or financial state"}},{"method":"GET","path":"/schemas/billing-customer-invoices-v1.json","auth":"public","description":"Strict customer-safe actual-charge invoice list, detail and download schema.","response":{"200":"BillingCustomerInvoicesV1 Draft 2020-12 JSON Schema"}},{"method":"GET","path":"/schemas/billing-customer-invoices-v1.example.json","auth":"public","description":"Synthetic issued and pending prepaid charge invoice examples.","response":{"200":"BillingCustomerInvoicesV1 conformance examples"}},{"method":"GET","path":"/schemas/billing-customer-invoices-v1.openapi.json","auth":"public","description":"Versioned OpenAPI 3.1 actual-charge invoice contract.","response":{"200":"BillingCustomerInvoicesV1 OpenAPI 3.1 components"}},{"method":"POST","path":"/billing/v1/invoices/list","description":"Lists accepted prepaid payments and actual monthly service invoices by the charge source month. A pending legal document remains visible without guessed tax or invoice number.","notes":"Optional X-UOA-Billing-Presentation: 1.5.0 and X-UOA-Billing-Locale select localized generated labels and exact amount displays only; signed bodies, financial snapshots and legal document bytes do not change.","auth":"Product customer_lifecycle app key plus exact endpoint-audience X-UOA-Actor and current team or organisation billing-manager authority","body":{"product":"string","organisation_id":"string","team_id":"string","user_id":"string","charge_month":"YYYY-MM UTC","limit":"optional integer 1–50","cursor":"optional opaque cursor"},"response":{"200":"BillingCustomerInvoicesListV1; no provider units or private markup"}},{"method":"POST","path":"/billing/v1/invoices/detail","description":"Reads one actual invoice or accepted prepaid charge with only the issuer-proven customer charges and legal document state.","notes":"Optional X-UOA-Billing-Presentation: 1.5.0 and X-UOA-Billing-Locale select localized generated labels and exact amount displays only; signed bodies, financial snapshots and legal document bytes do not change.","auth":"Product customer_lifecycle app key plus exact endpoint-audience X-UOA-Actor and current payer billing-manager authority","body":{"product":"string","organisation_id":"string","team_id":"string","user_id":"string","invoice_id":"string (from the list)"},"response":{"200":"BillingCustomerInvoiceDetailV1; pending documents have no download"}},{"method":"POST","path":"/billing/v1/invoices/download","description":"Streams the original verified legal PDF after exact payer reauthorization, never a projected or synthetic invoice.","auth":"Product customer_lifecycle app key plus exact endpoint-audience X-UOA-Actor and current payer billing-manager authority","body":{"product":"string","organisation_id":"string","team_id":"string","user_id":"string","invoice_id":"string","document_id":"string (from the detail action)"},"response":{"200":"Private application/pdf attachment"}},{"method":"GET","path":"/schemas/billing-cycles-v2.json","auth":"public","description":"Strict customer-safe monthly cycle list, detail, and download request schema.","response":{"200":"BillingCyclesV2 Draft 2020-12 JSON Schema"}},{"method":"GET","path":"/schemas/billing-cycles-v2.example.json","auth":"public","description":"Synthetic monthly cycle fixture with seat evidence, measured usage, credits and real document metadata.","response":{"200":"BillingCyclesV2 conformance examples"}},{"method":"GET","path":"/schemas/billing-cycles-v2.openapi.json","auth":"public","description":"Versioned OpenAPI 3.1 monthly billing cycle contract.","response":{"200":"BillingCyclesV2 OpenAPI 3.1 components"}},{"method":"POST","path":"/billing/v1/cycles/list","description":"Paginated, exact-product/team monthly billing history. Only actual frozen cycles and a clearly labeled open preview appear.","notes":"Optional X-UOA-Billing-Presentation: 1.5.0 and X-UOA-Billing-Locale select localized generated labels and exact amount displays only; signed bodies, financial snapshots and legal document bytes do not change.","auth":"Product customer_lifecycle app key plus exact endpoint-audience X-UOA-Actor; active billing manager for the frozen payer scope","body":{"product":"string","organisation_id":"string","team_id":"string","user_id":"string","limit":"optional integer 1–24","cursor":"optional earlier YYYY-MM month"},"response":{"200":"BillingCyclesListV2; no private cost or markup"}},{"method":"POST","path":"/billing/v1/cycles/detail","description":"Frozen seat, measured usage, credits, actual documents and adjustment lineage for one exact product/team cycle.","notes":"Optional X-UOA-Billing-Presentation: 1.5.0 and X-UOA-Billing-Locale select localized generated labels and exact amount displays only; signed bodies, financial snapshots and legal document bytes do not change.","auth":"Product customer_lifecycle app key plus exact endpoint-audience X-UOA-Actor and current payer billing-manager authority","body":{"product":"string","organisation_id":"string","team_id":"string","user_id":"string","cycle_id":"string (from the list)"},"response":{"200":"BillingCycleDetailV2; an open preview has no final invoice download"}},{"method":"POST","path":"/billing/v1/cycles/download","description":"Streams one immutable, SHA-verified customer PDF or CSV after exact scope and payer reauthorization.","auth":"Product customer_lifecycle app key plus exact endpoint-audience X-UOA-Actor and current payer billing-manager authority","body":{"product":"string","organisation_id":"string","team_id":"string","user_id":"string","cycle_id":"string","document_id":"string (from the cycle detail action)"},"response":{"200":"Private application/pdf or text/csv attachment; no external URL proxy"}},{"method":"GET","path":"/billing/v1/jwks.json","description":"Public JWKS for content-free effective-tariff snapshots. It publishes the current and overlapping retired public keys, and returns 404 until both tariff snapshot key variables are configured.","auth":"public","response":{"200":"{ keys: [current and overlapping retired public RS256 JWKs] }","404":"Tariff snapshot signing is not configured"}},{"method":"GET","path":"/billing/v1/service-jwks.json","description":"Public JWKS used only to verify UOA’s short-lived Ledger raw-metering service assertions. It contains the current and overlapping retired public keys and is separate from tariff-snapshot, OAuth-resource-token, product-app-key, and webhook credentials.","auth":"public","response":{"200":"{ keys: [current and overlapping retired public RS256 JWKs] }","404":"Ledger billing assertion signing is not configured"}},{"method":"GET","path":"/schemas/billing-statement-v1.json","description":"Public Draft 2020-12 schema for UOA’s exact display-ready canonical customer billing statement.","auth":"public","response":{"200":"BillingStatementV1 JSON Schema"}},{"method":"GET","path":"/schemas/billing-statement-v1.example.json","description":"Public synthetic BillingStatementV1 conformance fixture. It contains no production user, tenant, credential, or commercial data.","auth":"public","response":{"200":"Display-ready BillingStatementV1 example matching the exact v1 JSON Schema"}},{"method":"GET","path":"/schemas/billing-statement-v1.openapi.json","description":"Public OpenAPI 3.1 consumer artifact embedding the exact BillingStatementV1 JSON Schema and synthetic conformance fixture.","auth":"public","response":{"200":"Versioned OpenAPI 3.1 components.schemas and components.examples document"}},{"method":"GET","path":"/schemas/billing-statement-v2.json","description":"Public Draft 2020-12 schema for the SSO-filled customer statement and team-wide connected-service portfolio. Both statement routes use the privacy-revised customer contract.","auth":"public","response":{"200":"BillingStatementV2 JSON Schema"}},{"method":"GET","path":"/schemas/billing-statement-v2.example.json","description":"Public synthetic BillingStatementV2 conformance fixture with service, origin-product, and per-user transparency and no production data.","auth":"public","response":{"200":"Display-ready BillingStatementV2 example matching the exact v2 JSON Schema"}},{"method":"GET","path":"/schemas/billing-statement-v2.openapi.json","description":"Public OpenAPI 3.1 consumer artifact embedding the exact BillingStatementV2 JSON Schema and synthetic conformance fixture.","auth":"public","response":{"200":"Versioned OpenAPI 3.1 components.schemas and components.examples document"}},{"method":"GET","path":"/schemas/billing-consumer-actions-v1.json","description":"Public Draft 2020-12 schema bundle for the normalized hosted redirect, cancellation selection, exact preview and confirm_action, confirm request/response, and minimal error envelope. Every message object rejects additional properties.","auth":"public","response":{"200":"Exact billing consumer-action v1 JSON Schema components"}},{"method":"GET","path":"/schemas/billing-consumer-actions-v1.example.json","description":"Public synthetic conformance fixtures for every billing consumer-action message. They contain no production user, tenant, credential, or commercial data.","auth":"public","response":{"200":"Hosted redirect, cancellation preview/request/confirmation, and error fixtures"}},{"method":"GET","path":"/schemas/billing-consumer-actions-v1.openapi.json","description":"Public OpenAPI 3.1 component document embedding every exact billing consumer-action schema and synthetic fixture.","auth":"public","response":{"200":"Versioned OpenAPI 3.1 components.schemas and components.examples document"}},{"method":"GET","path":"/schemas/billing-credits-v1.json","description":"Public Draft 2020-12 schema for UOA’s display-ready shared-team credit balance, funding policy, automatic top-up state, and privacy-scoped usage projection.","auth":"public","response":{"200":"BillingCreditsV1 JSON Schema"}},{"method":"GET","path":"/schemas/billing-credits-v1.example.json","description":"Public synthetic BillingCreditsV1 conformance fixture. It contains no production user, tenant, credential, payment, or commercial data.","auth":"public","response":{"200":"Display-ready BillingCreditsV1 example matching the exact JSON Schema"}},{"method":"GET","path":"/schemas/billing-credits-v1.openapi.json","description":"Public OpenAPI 3.1 consumer artifact embedding the exact BillingCreditsV1 JSON Schema and synthetic conformance fixture.","auth":"public","response":{"200":"Versioned OpenAPI 3.1 schema and example document"}},{"method":"GET","path":"/schemas/billing-credit-funding-request-v1.json","description":"Public Draft 2020-12 schema for the exact-scope funding-help acknowledgement; it lists eligible UOA user IDs but does not claim a notification was delivered.","auth":"public","response":{"200":"BillingCreditFundingRequestV1 JSON Schema"}},{"method":"GET","path":"/schemas/billing-recurring-addons-v1.json","description":"Public Draft 2020-12 schema for UOA’s display-ready recurring add-on offers and exact-scope subscriptions.","auth":"public","response":{"200":"BillingRecurringAddonsV1 JSON Schema"}},{"method":"GET","path":"/schemas/billing-recurring-addons-v1.example.json","description":"Public synthetic manager/member BillingRecurringAddonsV1 conformance fixtures with no production data.","auth":"public","response":{"200":"Display-ready BillingRecurringAddonsV1 examples matching the exact JSON Schema"}},{"method":"GET","path":"/schemas/billing-recurring-addons-v1.openapi.json","description":"Public OpenAPI 3.1 consumer artifact embedding the exact recurring-add-on schema and synthetic fixtures.","auth":"public","response":{"200":"Versioned OpenAPI 3.1 schema and example document"}},{"method":"POST","path":"/billing/v1/credits","description":"Settle one exact team-wide Ledger portfolio cursor across every connected service and return the shared Remaining credits projection. Billing managers receive full team attribution and funding metadata; members receive only their own usage plus team-safe aggregates.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"200":"BillingCreditsV1 exactly matching /schemas/billing-credits-v1.json","409":"The Ledger cursor conflicts with an already-pinned exact-team snapshot","401/403":"Invalid purpose-bound app key, actor, product, membership, or subject","502/503":"Ledger portfolio evidence or Stripe collection context is unavailable"},"notes":"Settlement is deterministic, serializable, and cursor-idempotent. Available credits stop at zero while UOA retains the full centrally rated service/user liability; only a verified reversal may create debt. Products render the returned model and never rate or reallocate credits locally. With x-uoa-billing-credits-protocol: 2.1.0 the one-time offers are in the organisation billing region currency (USD, EUR or GBP; USD until a region is chosen) and a GBP or EUR offer carries credit_rate; older versions receive the USD offers. With 2.2.0 the automatic reload options are in that currency too, each with threshold_value (compared with the lot-valued balance) and credit_rate; older versions receive the USD options. With 2.3.0 credit_balance.value, the value of each recent entry and region_rate show what credits are worth in the region currency; charges stay in credits."},{"method":"POST","path":"/billing/v1/credits/funding-request","description":"Resolve current eligible billing managers for a member’s exact team credit account without sending a message or claiming one was sent.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"200":"BillingCreditFundingRequestV1: stable opaque request_id and current recipient_user_ids","400":"Billing Presentation 1.5.0 is required","401/403":"Invalid app key, actor, active membership, payer authority, or no eligible recipient","502/503":"Current billing context is unavailable"},"notes":"The caller cannot choose recipient IDs. UOA rechecks the active organisation/team membership, current payer responsibility, and manager roles on every call. The HMAC request ID is deduplicable by account, selected team, requester, and UTC day. Products map only the returned IDs to active local users before creating their own alerts; this source endpoint sends nothing."},{"method":"GET","path":"/schemas/billing-credit-purchase-status-v1.json","description":"Strict schema for exact authorized credit purchase status.","auth":"public","response":{"200":"BillingCreditPurchaseStatusV1 JSON Schema"}},{"method":"POST","path":"/billing/v1/credits/purchase-status","description":"Read one opaque credit purchase and optionally resume its exact still-open Checkout after current billing authority and binding checks.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","purchase_id":"exact opaque ID from a negotiated credit Checkout"},"response":{"200":"BillingCreditPurchaseStatusV1, optionally including the same purchase_id and safe Stripe Checkout continuation URL","404":"Purchase is not visible in this exact account, customer, service and app key","401/403":"Current actor or billing authority denied","502/503":"Current billing context is unavailable"},"notes":"Read-only financial status. Succeeded requires a durable credit entry and its verified completion event; Stripe payment success alone remains processing. An optional continuation is returned only for the exact bound Checkout while Stripe still reports it open; it carries the same purchase_id and never creates a new Checkout or charge. Presentation 1.5.0 headers select customer language; responses are private, no-store."},{"method":"POST","path":"/billing/v1/credits/top-up-checkout","description":"Create or recover secure Stripe Checkout for one exact active UOA credit offer.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","offer_id":"exact offer ID from the latest BillingCreditsV1"},"response":{"200":"{ redirect_url } for legacy clients; negotiated presentation adds the opaque purchase_id","409":"Offer/catalog unavailable or another exact-team Checkout is pending","401/403":"Invalid app key, actor, exact-team manager, or subject","502/503":"Stripe binding invalid, reconciliation pending, or 503 BILLING_CREDIT_FX_RATE_UNAVAILABLE for a GBP or EUR offer while its purchase rate cannot be read"},"notes":"The body is frozen: callers cannot supply an amount, price, currency, quantity, customer, metadata, or return URL. UOA persists immutable intent before Stripe and uses server-derived idempotency. A USD offer, or one in the organisation billing region currency, is accepted; a GBP or EUR checkout fixes its credits at the live rate read as it is created (BILLING_CREDIT_FX_RATE_SOURCE, default Stripe) and stores the rate as evidence."},{"method":"POST","path":"/billing/v1/credits/auto-top-up/setup","description":"Create or recover Setup Checkout for one exact UOA threshold/refill/monthly-cap option.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","option_id":"exact option ID from the latest BillingCreditsV1"},"response":{"200":"{ redirect_url } for the exact UOA-hosted Setup Checkout","409":"Option/catalog/state unavailable or another setup is pending","401/403":"Invalid app key, actor, exact-team manager, or subject","502/503":"Stripe binding invalid or reconciliation pending"},"notes":"UOA derives the Stripe customer, price evidence, consent terms, metadata, and allowlisted return URLs. The product relays only the frozen action body."},{"method":"POST","path":"/billing/v1/credits/auto-top-up/update","description":"Append an immutable manager consent revision selecting one exact active UOA option for the verified saved payment method.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","option_id":"exact option ID from the latest BillingCreditsV1"},"response":{"204":"Automatic top-up option selected idempotently","409":"Option/catalog, consent state, or payment method unavailable","401/403":"Invalid app key, actor, exact-team manager, or subject","502/503":"Stripe binding invalid or unavailable"}},{"method":"POST","path":"/billing/v1/credits/auto-top-up/disable","description":"Disable future automatic top-ups for the exact team without changing remaining credits.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"204":"Automatic top-up disabled idempotently","409":"Current consent or authorization conflicts with the requested change","503":"Stripe collection context unavailable","401/403":"Invalid app key, actor, exact-team manager, or subject"},"notes":"An unresolved payment does not prevent disabling future automatic charges. The original attempt stays bound to its immutable consent; late verified settlement cannot reactivate automatic top-up."},{"method":"POST","path":"/billing/v1/credits/auto-top-up/recover","description":"Return the verified HTTPS action URL for an exact unresolved automatic top-up or create bounded replacement Setup Checkout.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"200":"{ redirect_url } for Stripe-hosted recovery or UOA-created Setup Checkout","409":"No verified recovery is available or the payment is still processing","401/403":"Invalid app key, actor, exact-team manager, or subject","502/503":"Stripe metadata/customer/payment binding invalid or unavailable"},"notes":"UOA returns only a verified HTTPS URL bound to the exact immutable local attempt; callers cannot select an intent or recovery URL."},{"method":"POST","path":"/billing/v1/recurring-addons","description":"Return UOA-owned active recurring add-on offers and exact organisation/team/subscribing-user subscription projections for the requested product.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"200":"BillingRecurringAddonsV1 exactly matching /schemas/billing-recurring-addons-v1.json","503":"Stripe collection context is unavailable","401/403":"Invalid purpose-bound app key, actor, product, membership, or subject"},"notes":"Billing managers may see subscription identifiers and subscribing-user identity. Members receive relationship-only subscription visibility and no payment or other-user identity details. Enabled mutation actions contain complete frozen UOA request bodies."},{"method":"POST","path":"/billing/v1/recurring-addons/checkout","description":"Create or recover one exact, one-item monthly Stripe Checkout session for a UOA recurring add-on offer. UOA derives the customer, price, scope, return URLs, and idempotency identity.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","offer_id":"the exact active UOA recurring add-on offer ID returned by the read projection"},"response":{"200":"A normalized HTTPS Stripe Checkout redirect","409":"The offer, scope policy, customer, live subscription, or unresolved Checkout conflicts","503":"The exact UOA Stripe catalog or current Stripe binding is unavailable","401/403":"Invalid lifecycle credential, actor, membership, subject, or scope manager"},"notes":"Checkout completion creates only a pending local subscription projection. Entitlement activates only after an exact, undiscounted initial invoice.paid webhook is verified."},{"method":"POST","path":"/billing/v1/recurring-addons/cancellation/preview","description":"Refresh and preview period-end cancellation for one exact UOA recurring add-on subscription, returning an opaque five-minute capability and frozen confirm body.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","subscription_id":"the exact UOA subscription ID returned in a manager projection"},"response":{"200":"BillingRecurringAddonCancellationPreviewV1","409":"The subscription is terminal, already scheduled, changed, or already has a preview","503":"Current Stripe subscription evidence is unavailable or inconsistent","401/403":"Invalid lifecycle credential, actor, membership, subject, or scope manager"},"notes":"Only the token digest is stored. Exactly one AVAILABLE or PROCESSING preview may exist for a subscription."},{"method":"POST","path":"/billing/v1/recurring-addons/cancellation/confirm","description":"Consume one still-valid UOA recurring add-on cancellation preview and schedule the exact Stripe subscription to cancel at period end.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","preview_token":"the opaque short-lived UOA token returned by the cancellation preview","idempotency_key":"the UOA-issued idempotency key returned by the same preview","choice":"the literal cancel_addon choice frozen by UOA"},"response":{"200":"BillingRecurringAddonCancellationConfirmationV1, replayed byte-for-byte for the same request","409":"The token was reused with a different request or its subscription binding changed","503":"Current Stripe cancellation evidence is unavailable or inconsistent","401/403":"Invalid lifecycle credential, actor, membership, subject, or scope manager","404/410":"The opaque preview token is invalid or expired"},"notes":"UOA rechecks the actor and exact scope under a row lock, uses one stable Stripe idempotency key, and persists the exact result before acknowledging success."},{"method":"POST","path":"/billing/v1/organisation-billing","description":"Read whether the organisation has taken billing over from its teams, and whether this exact caller may manage it.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"200":"{ organisation_id, active, assumed_at, released_at, can_manage, billing_region: { region, currency, suggested_region, suggested_currency, suggested_from?, can_set } }","401/403":"Invalid lifecycle credential, actor, membership, or subject"},"notes":"Absent or inactive means each team pays for itself, which is the default everywhere. With x-uoa-billing-region-protocol: 1.1.0 the billing_region block (and the region set response) adds suggested_from: billing_country when the suggestion came from the invoice-profile billing country, null when no country is known and US is the default."},{"method":"POST","path":"/billing/v1/organisation-billing/region","description":"Choose the organisation billing region once: UK (GBP), US (USD) or EUROPE (EUR). It sets the currency of SMS prices, the SMS balance and number subscriptions.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","region":"UK | US | EUROPE"},"response":{"200":"{ region, currency, suggested_region, suggested_currency, suggested_from? (region protocol 1.1.0), can_set }","409":"BILLING_REGION_IMMUTABLE: a different region was already chosen","401/403":"Invalid lifecycle credential, actor, membership, or BILLING_ORG_MANAGER_REQUIRED"},"notes":"Set once, never changed; the database refuses updates. Repeating the same region is a no-op. AI usage stays USD for every region. Until chosen, SMS quotes, number purchases and SMS top-ups answer 409 BILLING_REGION_REQUIRED."},{"method":"POST","path":"/billing/v1/organisation-billing/assume","description":"Take billing over for every team in the organisation, across every service: credits, funding and the statement answer at the organisation from then on.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"200":"{ organisation_id, active: true, assumed_at, released_at: null, deactivated_team_auto_top_ups }","409":"FUNDING_ACTION_IN_FLIGHT with the exact rows, or TEAM_SUBSCRIPTIONS_ACTIVE with the exact live team subscriptions","401/403":"Invalid lifecycle credential, actor, membership, or not an organisation billing manager"},"notes":"Refused whole, never partially applied. Each team’s stored automatic top-up consent is deactivated (never deleted) with an audit row, and team credit balances are never swept. Live team subscriptions must be moved or ended by an operator: auto-migrating them would re-bill a customer without a decision."},{"method":"POST","path":"/billing/v1/organisation-billing/release","description":"Hand billing back to the teams. Reverses credit resolution only.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"200":"{ organisation_id, active: false, assumed_at, released_at }","401/403":"Invalid lifecycle credential, actor, membership, or not an organisation billing manager"},"notes":"Organisation-scoped ledger rows, invoices and statements stay where the spend was incurred; history is never re-scoped. Deactivated team auto-top-up consents stay inactive and require explicit re-consent."},{"method":"POST","path":"/billing/v1/effective-tariff","description":"Resolve team > organisation > service-default tariff precedence, re-check active UOA membership, and return a signed content-free entitlement snapshot. UOA rates money privately; products receive collection and subscription terms, not provider cost or price adjustment.","auth":"X-UOA-App-Key: uoa_app_… credential dedicated to the requested product, plus X-UOA-Actor: short-lived RS256 actor JWT bound to that credential, aud = this exact endpoint URL","body":{"product":"string (required) — exact global billing service identifier bound to the app key","organisation_id":"string (required)","team_id":"string (required)","user_id":"string (required)"},"response":{"200":"{ snapshot, payload } — snapshot is RS256 typ=uoa-tariff+jwt; payload contains schema/product/authorized app-key/subject, opaque tariff ID for UOA Checkout binding, collection_mode, monthly amount/currency, usage_billing_enabled, payment_collection_enabled, assignment scope, raw_usage_preserved=true, issued/expires timestamps; no private price basis","401/403":"Generic error for invalid/revoked/wrong-product app key, invalid actor signature, actor/body mismatch, or inactive membership"},"notes":"Actor claims: iss/aud exact credential values, sub=user_id, product, organisation_id, team_id, unique jti, iat/exp with maximum 60-second lifetime. Snapshot iss is PUBLIC_BASE_URL; aud is the credential actor_issuer. Consumers must verify the signature and require exact signed product ID+identifier, authorized app-key ID, and user/organisation/team subject binding; shared actor signers never make snapshots portable across products. UOA alone rates usage; payment_collection_enabled and collection_mode describe whether/how payment is collected."},{"method":"POST","path":"/billing/v1/customer-statement","description":"Return UOA’s display-ready canonical plan, subscription, customer charges, credit consumption, connected-service credit totals, exact commercial lines/totals, capabilities, and server-pinned actions. Token counts, provider costs and markup remain private. The statement pins immutable Ledger service/user snapshots and the exact UOA tariff version.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","billing_month":"optional UTC YYYY-MM; current or past only"},"response":{"200":"BillingStatementV1 exactly matching /schemas/billing-statement-v1.json","401/403":"Invalid purpose-bound app key, actor, product, membership, or subject","502/503":"Ledger raw metering is invalid/unavailable or its dedicated reader is unconfigured"},"notes":"Products render this model and proxy only whitelisted action ID/path pairs. They never derive totals, private price terms, direct access, or cancellation choices. The browser receives neither app key nor actor JWT."},{"method":"POST","path":"/billing/v2/customer-statement","description":"Return BillingStatementV2: the complete v1 commercial statement plus a display-ready, team-wide raw connected-service portfolio grouped by billing service, origin product, and user. UOA rates only the requested product; other-service totals are explanatory and never become charges on this statement.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact product identifier bound to the app key and portfolio perspective","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID","billing_month":"optional UTC YYYY-MM; current or past only"},"response":{"200":"BillingStatementV2 exactly matching /schemas/billing-statement-v2.json","401/403":"Invalid purpose-bound app key, actor, product, membership, or subject","502/503":"Ledger’s immutable metering-portfolio-v1 snapshot is invalid/unavailable or the dedicated UOA reader is unconfigured"},"notes":"UOA derives commercial rating plus every service, origin, and per-user total from one exact user-grouped metering-portfolio-v1 snapshot. Products render the title, descriptions, totals, origin shares, per-user shares, and action model verbatim. The portfolio uses origin_product for delegated attribution, so Nessie→DeepWater usage contributes to Nessie’s share without creating direct DeepWater access or a related cancellation choice. Null origins render as Unattributed origin and likewise create no access or choice. Products perform no aggregation, rating, or cancellation inference."},{"method":"POST","path":"/billing/v1/service-access/confirm","description":"Confirm UOA-owned direct product-access evidence immediately after that product’s successful UOA SSO exchange or session establishment. UOA rechecks exact product binding plus active organisation/team membership. Proxy or agent use of another product is indirect and must never invoke this endpoint for the other product.","auth":"The directly accessed product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; both remain backend-only","body":{"product":"exact unhyphenated product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID","user_id":"UOA user ID"},"response":{"204":"Direct session confirmed; no content","401/403":"Invalid key purpose, actor, product, subject, or active membership"},"notes":"Canonical identifiers are nessie, deepwater, deepsignal, deeptest, and docgen. Product/repository slugs such as deep-water or deep-test are mapped before this call. The browser receives neither credential."},{"method":"GET","path":"/internal/admin/billing/contracts","description":"List organisation contracts and immutable versions. This contract-editor response may show the organisation-wide usage markup; invoice responses never do. Server-authored contract.actions.add_version; clients render it and never reconstruct eligibility. Nested versions use Server-authored version.actions.{activation_state,activate}. ready is a future-month draft eligible for activation; scheduled is an activated future-month version. Other states are active | superseded | contract_terminated. Clients render these values and never reconstruct eligibility.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","query":{"organisation_id":"optional exact UOA organisation ID"},"response":{"200":"Contract array. Server-authored contract.actions.add_version; clients render it and never reconstruct eligibility. Nested versions use Server-authored version.actions.{activation_state,activate}. ready is a future-month draft eligible for activation; scheduled is an activated future-month version. Other states are active | superseded | contract_terminated. Clients render these values and never reconstruct eligibility."}},{"method":"POST","path":"/internal/admin/billing/contracts","description":"Create an inactive organisation contract header.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"organisation_id":"exact UOA organisation ID","reference":"stable lowercase operator reference","name":"operator-facing contract name"},"response":{"201":"Created draft contract. Server-authored contract.actions.add_version; clients render it and never reconstruct eligibility."}},{"method":"POST","path":"/internal/admin/billing/contracts/:contractId/versions","description":"Append an immutable forward-effective commercial version. Markup is visible only on this contract-editor surface.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"usage_markup_percent":"exact decimal percentage string with at most two fractional digits, applied centrally","currency":"exact three-letter ISO currency; no FX inference","payment_terms_days":"integer 0-365","effective_from_month":"UTC YYYY-MM, later than every existing version"},"response":{"201":"Created immutable version without active service terms. Server-authored version.actions.{activation_state,activate}. ready is a future-month draft eligible for activation; scheduled is an activated future-month version. Other states are active | superseded | contract_terminated. Clients render these values and never reconstruct eligibility."}},{"method":"POST","path":"/internal/admin/billing/contracts/:contractId/versions/:versionId/activate","description":"Schedule future-effective immutable CUSTOM+MANUAL organisation terms per selected service. Capture the UOA seat baseline now and track it until the commercial month. Reject carried-assignment drift, team overrides, and nonterminal Stripe state.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"services":"non-empty [{ service_id, monthly_amount_minor, monthly_charge_basis?, seat_policy?, seat_charge_timing?, usage_payment_mode?, fixed_seat_quantity? }]. New operator writes choose the basis and usage mode; fixed quantity is required only for fixed per-seat terms. Omitted fields retain legacy flat/pay-as-you-go semantics."},"response":{"200":"Activated contract version with service terms. Server-authored version.actions.{activation_state,activate}. ready is a future-month draft eligible for activation; scheduled is an activated future-month version. Other states are active | superseded | contract_terminated. Clients render these values and never reconstruct eligibility."}},{"method":"GET","path":"/internal/admin/billing/invoice-issuer-profiles","description":"List explicit legal issuer profiles. UOA never invents or seeds issuer identity.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row"},{"method":"GET","path":"/internal/admin/billing/credit-invoice-tax-policies","description":"List Stripe accounts and append-only legal issuer tax policies for accepted prepaid payment invoices. Platform superuser only.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row"},{"method":"POST","path":"/internal/admin/billing/credit-invoice-tax-policies","description":"Append a versioned issuer tax treatment effective at a UTC payment time. The tax amount is included within the actual accepted gross; this action never charges the payer.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"account_id":"exact Stripe account row ID","issuer_profile_id":"active explicit issuer with the same tax jurisdiction","jurisdiction_country":"two-letter uppercase issuer jurisdiction","treatment":"INCLUSIVE_RATE or NO_TAX_CHARGED","rate_bps":"integer inclusive tax rate 1-10000, or 0 for explicit no-tax treatment","legal_basis_reference":"operator-attested legal rule or exemption reference","effective_from":"UTC ISO timestamp, exact effective start"},"response":{"201":"Created immutable policy version and actor audit event"}},{"method":"POST","path":"/internal/admin/billing/invoice-issuer-profiles","description":"Create an explicit legal issuer and invoice-number prefix.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"key":"stable profile key","legal_name":"required legal name","billing_email":"required invoice email","address":"{ line1, line2?, city, region?, postal_code, country }","invoice_number_prefix":"uppercase letters/numbers/_/-","optional":"trading_name, tax_identifier, company_registration_number; tax is never inferred"},"response":{"201":"Created issuer profile"}},{"method":"GET","path":"/internal/admin/billing/organisations/:organisationId/invoice-profile","description":"Read the organisation buyer legal/billing profile used for invoice snapshots.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row"},{"method":"PUT","path":"/internal/admin/billing/organisations/:organisationId/invoice-profile","description":"Create or update the explicit buyer profile; issued invoices retain their snapshot.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"legal_name":"required buyer legal name","billing_email":"required accounts-payable email","billing_address":"{ line1, line2?, city, region?, postal_code, country }","optional":"tax_identifier, purchase_order_reference"}},{"method":"POST","path":"/internal/admin/billing/invoices/calculate","description":"Calculate a closed-month organisation invoice from one immutable org-scoped Ledger snapshot per contracted service. The response contains gross final price per service plus a separately labelled credits settlement; changed input creates the next immutable revision.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"contract_id":"active organisation contract ID","issuer_profile_id":"active explicit issuer profile ID","billing_month":"closed UTC YYYY-MM","tax_treatment":"no_tax_charged | standard_rate","tax_rate_percent":"exact 0..100 with at most two fractional digits","tax_legal_basis":"required legal basis frozen with the invoice"},"response":{"201":"Customer-safe draft invoice; never markup, cost, units, calls, cursor/hash, digest, or private PDF storage identity. Server-authored actions: issue is issue only after the database proves active contract/issuer, exact frozen evidence, collector exclusivity, and active-invoice uniqueness; it is resume_issue for recoverable issuance, otherwise null. download_pdf and void are booleans; any private positive credit-settlement reference blocks void even when its display rounds to zero. payment_limits contains nullable payment/refund/write_off maximum Money values. Clients render these values and never reconstruct lifecycle eligibility or payment caps. The private PDF object key and SHA-256 are never serialized."}},{"method":"GET","path":"/internal/admin/billing/cycle-corrections","description":"List closed-month manual billing cycle corrections awaiting a real legal financial effect.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","response":{"200":"Pending correction cycles with direction and optional supplement invoice id."}},{"method":"POST","path":"/internal/admin/billing/cycle-corrections/:cycleId/prepare","description":"Freeze a delta-only supplemental invoice from signed paid receipts, the original issued line and its tax evidence. No original monthly fee is repeated.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{},"response":{"201":"Customer-safe draft supplemental invoice ready for ordinary issue."}},{"method":"GET","path":"/internal/admin/billing/invoices","description":"List customer-safe contract invoice revisions with final per-service prices, separate credit/payment/write-off settlement totals, and payment status. Server-authored actions: issue is issue only after the database proves active contract/issuer, exact frozen evidence, collector exclusivity, and active-invoice uniqueness; it is resume_issue for recoverable issuance, otherwise null. download_pdf and void are booleans; any private positive credit-settlement reference blocks void even when its display rounds to zero. payment_limits contains nullable payment/refund/write_off maximum Money values. Clients render these values and never reconstruct lifecycle eligibility or payment caps. The private PDF object key and SHA-256 are never serialized.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","query":{"organisation_id":"optional","contract_id":"optional","billing_month":"optional UTC YYYY-MM","status":"optional draft | issuing | issued | void"},"response":{"200":"Customer-safe invoice array. Server-authored actions: issue is issue only after the database proves active contract/issuer, exact frozen evidence, collector exclusivity, and active-invoice uniqueness; it is resume_issue for recoverable issuance, otherwise null. download_pdf and void are booleans; any private positive credit-settlement reference blocks void even when its display rounds to zero. payment_limits contains nullable payment/refund/write_off maximum Money values. Clients render these values and never reconstruct lifecycle eligibility or payment caps. The private PDF object key and SHA-256 are never serialized."}},{"method":"GET","path":"/internal/admin/billing/invoices/:invoiceId","description":"Read one customer-safe invoice. Private Ledger references, internal calculation inputs, and private PDF storage identity are never serialized. Server-authored actions: issue is issue only after the database proves active contract/issuer, exact frozen evidence, collector exclusivity, and active-invoice uniqueness; it is resume_issue for recoverable issuance, otherwise null. download_pdf and void are booleans; any private positive credit-settlement reference blocks void even when its display rounds to zero. payment_limits contains nullable payment/refund/write_off maximum Money values. Clients render these values and never reconstruct lifecycle eligibility or payment caps. The private PDF object key and SHA-256 are never serialized.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","response":{"200":"Customer-safe invoice. Server-authored actions: issue is issue only after the database proves active contract/issuer, exact frozen evidence, collector exclusivity, and active-invoice uniqueness; it is resume_issue for recoverable issuance, otherwise null. download_pdf and void are booleans; any private positive credit-settlement reference blocks void even when its display rounds to zero. payment_limits contains nullable payment/refund/write_off maximum Money values. Clients render these values and never reconstruct lifecycle eligibility or payment caps. The private PDF object key and SHA-256 are never serialized."}},{"method":"GET","path":"/internal/admin/billing/invoices/:invoiceId/credit-note","description":"Read the separate issuer cancellation credit note for one issued manual invoice.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","response":{"200":"Credit-note status, number, date, original invoice, exact customer credit and reason; or null."}},{"method":"POST","path":"/internal/admin/billing/invoices/:invoiceId/credit-note/prepare","description":"Prepare one full-line legal cancellation note for a paid, issued, single-product manual invoice without wallet offsets. The original invoice remains immutable.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"reason":"required operator cancellation reason, max 500 characters"},"response":{"201":"Frozen pending credit note with exact original charge and tax."}},{"method":"POST","path":"/internal/admin/billing/credit-notes/:creditNoteId/issue","description":"Allocate a separate legal number and immutable PDF to the prepared issuer cancellation; durable cycle reconciliation follows.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{},"response":{"200":"Issued credit-note metadata; original accepted payment is preserved."}},{"method":"GET","path":"/internal/admin/billing/credit-notes/:creditNoteId/pdf","description":"Download the numbered legal credit note after bounded PDF SHA-256 verification.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row"},{"method":"POST","path":"/internal/admin/billing/invoices/:invoiceId/issue","description":"Idempotently allocate a serial invoice number, generate/store a wrapping-safe Unicode private immutable PDF, and issue the exact frozen draft revision.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{},"response":{"200":"Customer-safe issued or recoverable issuing invoice. Server-authored actions: issue is issue only after the database proves active contract/issuer, exact frozen evidence, collector exclusivity, and active-invoice uniqueness; it is resume_issue for recoverable issuance, otherwise null. download_pdf and void are booleans; any private positive credit-settlement reference blocks void even when its display rounds to zero. payment_limits contains nullable payment/refund/write_off maximum Money values. Clients render these values and never reconstruct lifecycle eligibility or payment caps. The private PDF object key and SHA-256 are never serialized."}},{"method":"GET","path":"/internal/admin/billing/invoices/:invoiceId/pdf","description":"Stream an issued/void immutable PDF after SHA-256 verification. Response is private, no-store and final-price-only.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row"},{"method":"POST","path":"/internal/admin/billing/invoices/:invoiceId/void","description":"Void an unpaid issued invoice without deleting or reusing its number/PDF. Settled invoices cannot be voided.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"reason":"required audit reason, max 500"},"response":{"200":"Customer-safe void invoice. Server-authored actions: issue is issue only after the database proves active contract/issuer, exact frozen evidence, collector exclusivity, and active-invoice uniqueness; it is resume_issue for recoverable issuance, otherwise null. download_pdf and void are booleans; any private positive credit-settlement reference blocks void even when its display rounds to zero. payment_limits contains nullable payment/refund/write_off maximum Money values. Clients render these values and never reconstruct lifecycle eligibility or payment caps. The private PDF object key and SHA-256 are never serialized."}},{"method":"POST","path":"/internal/admin/billing/invoices/:invoiceId/payments","description":"Append an idempotent manual payment, refund, or write-off event without changing service prices.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"kind":"payment | refund | write_off","amount_minor":"positive integer string in invoice currency","currency":"exact invoice currency","idempotency_key":"required stable key","reference":"optional external reference","occurred_at":"ISO timestamp"},"response":{"201":"Customer-safe invoice with updated separate settlement totals. Server-authored actions: issue is issue only after the database proves active contract/issuer, exact frozen evidence, collector exclusivity, and active-invoice uniqueness; it is resume_issue for recoverable issuance, otherwise null. download_pdf and void are booleans; any private positive credit-settlement reference blocks void even when its display rounds to zero. payment_limits contains nullable payment/refund/write_off maximum Money values. Clients render these values and never reconstruct lifecycle eligibility or payment caps. The private PDF object key and SHA-256 are never serialized."}},{"method":"GET","path":"/internal/admin/billing/services","description":"List billing services with immutable tariff versions, org/team assignments, masked app-key metadata, Stripe catalog readiness, and subscription state","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row"},{"method":"POST","path":"/internal/admin/billing/services","description":"Create a global billable product/service and its required version-1 default tariff","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"identifier":"lowercase stable identifier (required)","name":"string (required, max 120)","default_tariff":"{\"key\":\"string (required, stable tariff family key)\",\"name\":\"string (required, max 120)\",\"mode\":\"standard | free | at_cost | custom\",\"collection_mode\":\"stripe | manual | none; free requires none; none preserves rating/visibility without collecting payment\",\"markup_percent\":\"optional exact decimal percentage string, at most two fractional digits; standard defaults to 30.00; custom requires explicit; free and at_cost = 0.00\",\"monthly_subscription\":\"{ amount_minor: non-negative integer string, currency: uppercase ISO currency, charge_basis: flat | per_seat, seat_policy: automatic | fixed and seat_charge_timing: full_month | prorated for per_seat only }\",\"usage_payment_mode\":\"prepaid | pay_as_you_go, independent of monthly subscription\"}"},"response":{"201":"Created service including its default tariff"}},{"method":"POST","path":"/internal/admin/billing/services/:serviceId/tariffs","description":"Append an immutable version to a tariff family; existing snapshots and assignments remain pinned to their recorded version","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"key":"string (required, stable tariff family key)","name":"string (required, max 120)","mode":"standard | free | at_cost | custom","collection_mode":"stripe | manual | none; free requires none; none preserves rating/visibility without collecting payment","markup_percent":"optional exact decimal percentage string, at most two fractional digits; standard defaults to 30.00; custom requires explicit; free and at_cost = 0.00","monthly_subscription":"{ amount_minor: non-negative integer string, currency: uppercase ISO currency, charge_basis: flat | per_seat, seat_policy: automatic | fixed and seat_charge_timing: full_month | prorated for per_seat only }","usage_payment_mode":"prepaid | pay_as_you_go, independent of monthly subscription","set_as_default":"boolean (optional, default false)"},"response":{"201":"Created tariff version"}},{"method":"PUT","path":"/internal/admin/billing/services/:serviceId/default-tariff","description":"Change the service default pointer to an existing tariff version","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"tariff_id":"string (required; tariff must belong to service)"}},{"method":"PUT","path":"/internal/admin/billing/services/:serviceId/assignments","description":"Upsert an organisation or team tariff assignment. Supplying team_id creates the higher-precedence team assignment.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"tariff_id":"string (required; tariff must belong to service)","organisation_id":"string (required)","team_id":"string | null (optional; when set, team must belong to organisation)"}},{"method":"DELETE","path":"/internal/admin/billing/services/:serviceId/assignments/:assignmentId","description":"Remove an org/team tariff assignment; lower precedence applies immediately","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","response":{"204":"No content"}},{"method":"POST","path":"/internal/admin/billing/services/:serviceId/adjustments","description":"Create an audited exact organisation/team add-on or credit owned by UOA. Amount is a non-negative minor-currency integer; sign comes from kind.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"organisation_id":"string (required)","team_id":"string | null (optional)","key":"stable lowercase line key","name":"display label","kind":"add_on | credit","cadence":"one_time | monthly","amount_minor":"non-negative integer string","currency":"three-letter ISO currency","starts_at":"ISO timestamp","ends_at":"later ISO timestamp | null"},"response":{"201":"Created commercial adjustment"}},{"method":"DELETE","path":"/internal/admin/billing/services/:serviceId/adjustments/:adjustmentId","description":"Deactivate and audit a commercial line without deleting its statement history.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","response":{"204":"No content"}},{"method":"GET","path":"/internal/admin/billing/services/:serviceId/app-keys","description":"List masked product-dedicated app keys, endpoint purpose, return-origin policy, and actor-signing bindings; no secret or public-key material is returned","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row"},{"method":"POST","path":"/internal/admin/billing/services/:serviceId/app-keys","description":"Mint a product-dedicated app key and bind an RS256 actor public JWK. The plaintext key is returned once and only its peppered digest is stored.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"purpose":"entitlement | customer_lifecycle (required); endpoint classes are mutually exclusive","name":"string (required, max 120)","actor_issuer":"HTTPS origin/URI (required)","actor_audience":"HTTPS UOA effective-tariff endpoint audience (required)","actor_public_jwk":"public RSA JWK (required; kid, n, e; alg RS256/use sig when present; private members forbidden)","checkout_return_origins":"array of up to 10 exact HTTPS origins; required and non-empty for customer_lifecycle, forbidden for entitlement","expires_at":"ISO timestamp | null (optional)"},"response":{"201":"Masked metadata plus one-time plaintext key (uoa_app_…)"}},{"method":"DELETE","path":"/internal/admin/billing/services/:serviceId/app-keys/:keyId","description":"Revoke a product app key immediately","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","response":{"204":"No content"}},{"method":"POST","path":"/billing/v1/stripe/checkout-session","description":"Create or recover one account/mode-scoped Stripe-hosted subscription Checkout lease for the effective immutable tariff. Org/default tariffs bill at organisation scope and exclude team subscriptions; independent team scopes may coexist. Only active org/team billing managers may start Checkout.","auth":"The requested product’s own X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL; never a shared product key","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID used for entitlement resolution","user_id":"UOA user ID; must be owner/admin at the resulting billing scope","success_url":"HTTPS URL whose exact origin is allowlisted on this app key","cancel_url":"HTTPS URL whose exact origin is allowlisted on this app key"},"response":{"201":"{ checkout_session_id, checkout_url, expires_at, tariff }","503":"Stripe billing is explicitly disabled or not fully provisioned","400/401/403/409":"Invalid caller/actor/return origin, non-Stripe/free tariff, non-manager, existing subscription, open Checkout, or replay mismatch"},"notes":"Requires a customer_lifecycle app key. The Checkout pins the exact immutable tariff version, precedence source, assignment, and billing scope until its subscription is terminal. It anchors to the first day of the next UTC month with proration disabled, so the alignment stub is free and the first invoice covers the first complete calendar month. It contains the monthly Price when non-zero plus exactly one currency-specific metered Price. That meter receives customer-rated money as integer micro-minor-currency units; it never receives or relabels raw tokens, searches, or research units. Promotions and all other discounts are disabled because UOA tariff versions are the sole commercial authority."},{"method":"POST","path":"/billing/v1/stripe/subscription-summary","description":"Return the effective tariff, caller management permission, and local current subscription projection without exposing Stripe identifiers. When collection is enabled, current Stripe state is refreshed first; when disabled, the last unambiguous projection is returned with stripe_collection_enabled=false.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID used for entitlement resolution","user_id":"UOA user ID"},"response":{"200":"{ product, subject, tariff, assignment, stripe_collection_enabled, stripe_mode, can_manage, subscription }","409":"The disabled deployment has multiple account projections and cannot select one safely","401/403":"Invalid purpose-bound app key, actor, product, membership, or subject"},"notes":"subscription.billing_phase is calendar_month, free_alignment_period, or unknown. All successful responses are private, no-store."},{"method":"POST","path":"/billing/v1/stripe/portal-session","description":"Create a Stripe-hosted customer portal session for the exact current billing scope. The actor must be an organisation owner/admin or the selected team’s admin.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID used for entitlement resolution","user_id":"UOA user ID; must manage the resulting billing scope","return_url":"HTTPS URL whose exact origin is allowlisted on this app key"},"response":{"201":"{ portal_url }","503":"Stripe billing is explicitly disabled or not fully provisioned","400/401/403/404":"Invalid redirect/caller/actor, non-manager, or no subscription"}},{"method":"POST","path":"/billing/v1/cancellation/preview","description":"Return the complete cancellation confirmation model and a five-minute opaque single-use preview capability. Related direct subscriptions are offered only when UOA has direct team-user entitlement evidence; Ledger-only indirect services never become a choice.","auth":"The requested product’s customer_lifecycle X-UOA-App-Key plus a fresh credential-bound X-UOA-Actor assertion whose aud is this exact endpoint URL","body":{"product":"exact product identifier bound to the app key","organisation_id":"UOA organisation ID","team_id":"UOA team ID used for entitlement resolution","user_id":"UOA user ID; must manage the current billing scope"},"response":{"201":"{ schema_version, preview_token, expires_at, title, message, choice_required, choices, direct_services, indirect_services, confirm_action }","503":"Stripe billing is explicitly disabled or not fully provisioned","401/403/404":"Invalid caller/actor, non-manager, or no subscription"}},{"method":"POST","path":"/billing/v1/cancellation/confirm","description":"Under a serializable row lock, bind and revalidate the preview’s exact entitlement/subscription state, schedule the selected pinned direct subscriptions to cancel at period end, and persist one idempotently replayable result.","auth":"The same requested product customer_lifecycle X-UOA-App-Key and a fresh bound X-UOA-Actor assertion whose aud is this exact endpoint URL","body":{"product":"exact product identifier bound to the app key","organisation_id":"exact preview organisation","team_id":"exact preview team","user_id":"exact preview user","preview_token":"opaque token returned once by preview","idempotency_key":"server-generated key from preview.confirm_action","selection":"current_service | current_and_related_direct_services | null as allowed by preview"},"response":{"200":"{ schema_version, status=confirmed, title, message, cancelled_services, indirect_services }","503":"Stripe billing is explicitly disabled or not fully provisioned","400/403/404/409/410":"Invalid binding/choice, already-used token mismatch, changed state, or expired token"}},{"method":"POST","path":"/billing/v1/stripe/webhook","description":"Verify Stripe’s signature over the exact raw request body, retrieve current account/mode state, and reconcile exact UOA app-key, Checkout, tariff source/assignment, customer, scope, and undiscounted item bindings before idempotently committing the event. Reordered subscription events cannot resurrect canceled state. A draft subscription-cycle invoice.created event performs the authoritative post-period Ledger export before event commit and requires at least one hour of automatic-finalization grace; invoice.finalization_failed is recorded and logged with structured status.","auth":"Stripe-Signature with the dedicated STRIPE_WEBHOOK_SECRET; webhook signing secrets are never product app keys","response":{"200":"{ received: true }; already committed event IDs are acknowledged without reapplying state","400":"Missing/invalid signature or body"}},{"method":"POST","path":"/internal/admin/billing/stripe/usage-exports","description":"Fetch one immutable Ledger metering-usage-v1 monthly snapshot through UOA’s own dedicated raw-metering reader key, validate its exact product/scope, rate raw provider cost under UOA’s pinned tariff, and idempotently export customer-money deltas to Stripe’s meter.","auth":"Authorization: Bearer <access_token>; token must be an ADMIN_AUTH_DOMAIN platform superuser and remain backed by a SUPERUSER domain_roles row","body":{"subscription_id":"UOA BillingStripeSubscription ID","billing_month":"UTC month in YYYY-MM","ledger_snapshot_cursor":"optional immutable mus_… cursor for exact Ledger replay; omitted creates a fresh snapshot"},"response":{"200":"{ ledger_snapshot_cursor, billing_month, exports[] } with separately labeled billing_product/caller_product, exact cumulative customer charge, cumulative/delta integer micro-minor meter quantities, stable Stripe event ID, and delivery timestamp","400/404/409/502/503":"Invalid month/subscription, stale or mismatched Ledger snapshot, non-meterable tariff/period, disabled collector/Stripe, or upstream failure"},"notes":"Platform-superuser/manual reconciliation endpoint. Ledger returns only raw usage/provider cost and attribution; UOA alone calculates the customer amount. Stripe events meter that exact money only, never raw tokens/searches/research units. Every call to Ledger uses UOA’s own X-Ledger-App-Key plus a fresh scope=metering.read X-UOA-Service-Assertion; the product app key that initiated Checkout is never reused. When Stripe collection is enabled, the in-process scheduler runs this same idempotent export for each active Stripe-paid full UTC month and schedules an additional pre-boundary safety pass. The authoritative post-period pass runs on a verified draft subscription-cycle invoice.created webhook whose automatic-finalization window is at least one hour, and must succeed before the webhook event is committed; failures remain retryable during Stripe’s finalization grace period. Meter-event creation is durable delivery evidence, not immediate aggregate visibility. The initial no-proration alignment stub and free/manual/none tariffs are never exported."},{"method":"GET","path":"/apps/startup","description":"Server-facing startup payload combining kill switch state and resolved feature flags","auth":"signed RS256 config JWT fetched from config_url, same verification path as /auth/login and /auth/register","query":{"config_url":"string (required) — HTTPS URL to fetch signed config JWT","appIdentifier":"string (required) — registered app identifier, e.g. com.acme.ios","platform":"string (required) — ios | android | web | macos | windows | other","versionName":"string (optional) — semantic/display version","versionCode":"string (optional) — Android numeric version code","buildNumber":"string (optional) — iOS/macOS build number","userId":"string (optional) — applies per-user flag overrides and kill-switch test targeting","teamId":"string (optional) — exact active UOA team context for flag resolution"},"response":{"killSwitch":"object|null — matched kill-switch entry, or null when clear","flags":"object — all resolved feature flags for the app as key:boolean","cacheTtl":"number — seconds the caller may cache the response","serverTime":"string — ISO timestamp","activatesIn":"number (optional) — seconds until a pending kill switch activates"}},{"method":"GET","path":"/apps/:appId/flags","description":"Backend-only real-time feature flags for one active App, UOA user subject, and optional exact active team. Wrong/inactive app, domain, membership, or disabled flag service returns an empty map without enumeration.","auth":"Authorization: Bearer SHA256(normalized config domain + full per-domain client secret)","query":{"domain":"string (required) — exact config domain registered on the App; for DeepWater this is api.deepwater.live","userId":"string (required) — stable UOA access-token sub / User.id","teamId":"string (optional) — exact UOA Team.id; callers with an active team should supply it"},"response":{"200":"flat object of resolved flag keys to booleans; Cache-Control: private, no-store","401":"missing, invalid, inactive, or wrong-domain backend credential"}},{"method":"POST","path":"/email/send","description":"Send a per-domain transactional email through UOA-managed email infrastructure","auth":"X-UOA-Config-JWT header containing a signed RS256 client config JWT","body":{"to":"email address (required)","subject":"string (required)","text":"string (required)","html":"string (optional)","reply_to":"email address (optional; overrides the configured default reply-to)"},"response":{"202":"{ ok: true }","401/403":"generic error for missing/invalid config JWT or unconfigured/unverified domain email"}},{"method":"GET","path":"/domain/teams/resolve","description":"Resolve a tenant hostname to ids. Hostnames are <team.slug>.<organisation.slug>.<product base domain>, so both labels are required: a team slug is unique only within its organisation and never identifies a tenant on its own.","auth":"domain hash bearer token","query":{"domain":"string (required)","org":"string (required) — the organisation slug, i.e. the tenant label","team":"string (required) — the team slug, read relative to that organisation"},"response":{"org_id":"string","org_name":"string","org_slug":"string","team_id":"string","team_name":"string","team_slug":"string"},"notes":"Domain-hash rather than /org/* deliberately: a product resolves a hostname before anyone has an active team, so there is no session to assert a subject from. Resolution is scoped to the calling client domain. An unknown organisation and a known organisation with an unknown team both answer the generic 404, so a tenant's team list is not readable from outside it."},{"method":"GET","path":"/domain/organisations/resolve","description":"Resolve a tenant's own hostname label to its organisation, with the name and icon a branded landing page needs.","auth":"domain hash bearer token","query":{"domain":"string (required)","org":"string (required) — the organisation slug, i.e. the tenant label"},"response":{"org_id":"string","org_name":"string","org_slug":"string","org_icon_url":"string | null — the organisation mark, null when unset"},"notes":"The organisation ONLY. Its teams are deliberately not listed: the label is guessable, and a product must not turn a guessable hostname into a directory of a customer's internal structure. Show a person the teams they belong to from their own membership, after they authenticate. Resolving still discloses that an organisation of this name exists on this domain, which is inherent in giving a tenant a public address."},{"method":"GET","path":"/domain/organisations/:id/address","description":"An organisation's own hostname label, from its id — the inverse of /domain/organisations/resolve.","auth":"domain hash bearer token","query":{"domain":"string (required)"},"response":{"org_id":"string","org_name":"string","org_slug":"string","org_icon_url":"string | null"},"notes":"A product stores UOA ids and no slug of its own, because the slug belongs to UOA — so without this a product can route a hostname it was handed but cannot build one. Scoped to the calling client domain like every /domain/* read."},{"method":"GET","path":"/domain/teams/:id/address","description":"Both labels of a team's address, from the team's id: the team slug and its organisation's.","auth":"domain hash bearer token","query":{"domain":"string (required)"},"response":{"team_id":"string","team_name":"string","team_slug":"string","org_id":"string","org_name":"string","org_slug":"string"},"notes":"Returns the pair in one read because a team hostname is <team.slug>.<organisation.slug>.<base domain> and a caller holds neither label — a picker that moves the address bar would otherwise make a request per row. Team ids are globally unique, so the read is confined by an organisation-domain predicate rather than by the id alone."},{"method":"GET","path":"/domain/slug-available","description":"Whether a slug may be taken, for the address field in a create dialog. Answers with a reason rather than a bare boolean.","auth":"domain hash bearer token","query":{"domain":"string (required)","slug":"string (required)","scope":"\"organisation\" | \"team\" (required)","org_id":"string (required when scope=team) — availability is per organisation, never global","reserved":"string (optional) — comma-separated product hostnames to reserve on top of the base list"},"response":{"available":"boolean","slug":"string — the normalised label, present when available","reason":"\"taken\" | \"too_short\" | \"too_long\" | \"charset\" | \"double_hyphen\" | \"all_digits\" | \"reserved\" — present when unavailable"},"notes":"Team scope is bounded by org_id so the check cannot be used to enumerate another customer's team names. An invalid label is answered without touching the database."},{"method":"GET","path":"/domain/users","description":"List users for a domain or resolve one exact UOA subject in that domain","auth":"domain hash bearer token","query":{"domain":"string (required)","limit":"number (optional)","user_id":"string (optional): exact subject filter; domain visibility still required"},"response":{"users[].avatar_url":"string | null — the external provider avatar URL, overwritten on every social login (unchanged)","users[].avatar_source":"\"uploaded\" | \"provider\" | \"generated\" — which source GET /domain/users/:userId/avatar would serve from","users[].avatar_image_url":"string — absolute URL that always resolves to this user's avatar image, fetchable with the same domain hash bearer"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/domain/logs","description":"Login logs for a domain","auth":"domain hash bearer token","query":{"domain":"string (required)","limit":"number (optional)"},"response":{"logs[].avatar_image_url":"string | null — the logged-in user's avatar image URL, fetchable with the same domain hash bearer; null on rows with no user_id"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/domain/debug","description":"Debug info (requires debug_enabled in config)","auth":"domain hash bearer token","query":{"domain":"string (required)"},"response":{"superuser.avatar_image_url":"string — the calling superuser's avatar image URL, fetchable with the same domain hash bearer","client_id":"string — the ClientDomain record id (a cuid), stable across client-secret rotation; NOT the domain-hash bearer credential and NOT the SHA256(domain + clientSecret) value carried as the client_id claim in access tokens — do not join them"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/domain/users/:userId/avatar","description":"Image bytes for a user visible to the authenticated domain. Unknown or cross-domain user ids return the standard generic 404 (same visibility as GET /domain/users).","auth":"domain hash bearer token","query":{"domain":"string (required)","config_url?":"string — optional; when supplied the signed config is fetched and verified AFTER the bearer check and its avatars.default_style is applied. Its domain claim must match ?domain=. An unauthenticated request is rejected before any fetch is attempted.","style?":"string — tiles | waves | rings | mono; overrides the config default and the per-user pick. Only affects generated avatars.","size?":"number — generated SVG width/height, clamped 16-512 (default 128). The viewBox is constant; ignored for raster images."},"response":{"content-type":"image/svg+xml; charset=utf-8 for generated avatars, or the stored/sniffed raster type (image/png | image/jpeg | image/webp) for uploaded and proxied provider images","X-UOA-Avatar-Source":"\"uploaded\" | \"provider\" | \"generated\" — where the returned bytes came from","Cache-Control":"private, max-age=300 (uploaded/proxied) or private, max-age=86400 (generated, deterministic)","ETag":"SHA-256 of the returned bytes; a matching If-None-Match gets 304","X-Content-Type-Options":"nosniff","Content-Disposition":"inline; filename=\"avatar.<ext>\"","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline' — SVG responses only","body":"raw image bytes — avatar GETs never return JSON"},"notes":"Resolution precedence is fixed: uploaded image → server-side proxy of the provider avatar URL (User.avatarUrl) → deterministic generated SVG. The provider fetch is HTTPS-only, SSRF-guarded, ~5s/5 MiB capped, and any failure silently falls back to the generated image, so a known user always yields 200 with an image. Provider bytes are never stored."},{"method":"PUT","path":"/domain/users/:userId/avatar","description":"Set a user's uploaded avatar from a multipart upload. Replaces any existing upload. Audit-logged as domain.user_avatar_updated against the acting domain. Rate-limited per domain+user (30/hour).","auth":"domain hash bearer token","query":{"domain":"string (required)"},"body":{"file":"multipart/form-data file part — PNG, JPEG or WebP, max 1 MiB. The type is decided by magic-byte sniffing, not the supplied mimetype; SVG and any non-raster upload is rejected with a generic error."},"response":{"ok":"true","avatar.source":"\"uploaded\"","avatar.content_type":"string — the sniffed type that was stored","avatar.size_bytes":"number","avatar.updated_at":"string — ISO timestamp"}},{"method":"DELETE","path":"/domain/users/:userId/avatar","description":"Remove a user's uploaded avatar; resolution falls back to the provider URL or the generated image. Idempotent. Audit-logged as domain.user_avatar_deleted against the acting domain. Rate-limited per domain+user (30/hour).","auth":"domain hash bearer token","query":{"domain":"string (required)"},"response":{"ok":"true"}},{"method":"GET","path":"/avatar/me","description":"Image bytes for the access-token subject — the end user's own avatar, relayed by a product backend.","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim","config_url?":"string — optional; applies the domain's avatars.default_style. Fetched and verified only AFTER both bearers check out; an unauthenticated request is rejected before any fetch is attempted.","style?":"string — tiles | waves | rings | mono; overrides the config default and the per-user pick. Only affects generated avatars.","size?":"number — generated SVG width/height, clamped 16-512 (default 128). The viewBox is constant; ignored for raster images."},"response":{"content-type":"image/svg+xml; charset=utf-8 for generated avatars, or the stored/sniffed raster type (image/png | image/jpeg | image/webp) for uploaded and proxied provider images","X-UOA-Avatar-Source":"\"uploaded\" | \"provider\" | \"generated\" — where the returned bytes came from","Cache-Control":"private, max-age=300 (uploaded/proxied) or private, max-age=86400 (generated, deterministic)","ETag":"SHA-256 of the returned bytes; a matching If-None-Match gets 304","X-Content-Type-Options":"nosniff","Content-Disposition":"inline; filename=\"avatar.<ext>\"","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline' — SVG responses only","body":"raw image bytes — avatar GETs never return JSON"},"notes":"Resolution precedence is fixed: uploaded image → server-side proxy of the provider avatar URL (User.avatarUrl) → deterministic generated SVG. The provider fetch is HTTPS-only, SSRF-guarded, ~5s/5 MiB capped, and any failure silently falls back to the generated image, so a known user always yields 200 with an image. Provider bytes are never stored."},{"method":"PUT","path":"/avatar/me","description":"Set the caller's own avatar from a multipart upload. The acting identity is always the access-token subject. Rate-limited per domain+user (30/hour).","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"body":{"file":"multipart/form-data file part — PNG, JPEG or WebP, max 1 MiB. The type is decided by magic-byte sniffing, not the supplied mimetype; SVG and any non-raster upload is rejected with a generic error."},"response":{"ok":"true","avatar.source":"\"uploaded\"","avatar.content_type":"string — the sniffed type that was stored","avatar.size_bytes":"number","avatar.updated_at":"string — ISO timestamp"}},{"method":"DELETE","path":"/avatar/me","description":"Clear the caller's own uploaded avatar. Rate-limited per domain+user (30/hour).","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"response":{"ok":"true"}},{"method":"GET","path":"/internal/admin/users/:userId/avatar","description":"Image bytes for any user, for the admin panel. Same resolution pipeline.","auth":"admin superuser bearer token","query":{"style?":"string — tiles | waves | rings | mono; overrides the config default and the per-user pick. Only affects generated avatars.","size?":"number — generated SVG width/height, clamped 16-512 (default 128). The viewBox is constant; ignored for raster images."},"response":{"content-type":"image/svg+xml; charset=utf-8 for generated avatars, or the stored/sniffed raster type (image/png | image/jpeg | image/webp) for uploaded and proxied provider images","X-UOA-Avatar-Source":"\"uploaded\" | \"provider\" | \"generated\" — where the returned bytes came from","Cache-Control":"private, max-age=300 (uploaded/proxied) or private, max-age=86400 (generated, deterministic)","ETag":"SHA-256 of the returned bytes; a matching If-None-Match gets 304","X-Content-Type-Options":"nosniff","Content-Disposition":"inline; filename=\"avatar.<ext>\"","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline' — SVG responses only","body":"raw image bytes — avatar GETs never return JSON"},"notes":"Resolution precedence is fixed: uploaded image → server-side proxy of the provider avatar URL (User.avatarUrl) → deterministic generated SVG. The provider fetch is HTTPS-only, SSRF-guarded, ~5s/5 MiB capped, and any failure silently falls back to the generated image, so a known user always yields 200 with an image. Provider bytes are never stored."},{"method":"PUT","path":"/internal/admin/users/:userId/avatar","description":"Set any user's uploaded avatar from a multipart upload, for the admin panel. Audit-logged as user.avatar_updated against the user's domain. Rate-limited per user (30/hour).","auth":"admin superuser bearer token","body":{"file":"multipart/form-data file part — PNG, JPEG or WebP, max 1 MiB. The type is decided by magic-byte sniffing, not the supplied mimetype; SVG and any non-raster upload is rejected with a generic error."},"response":{"ok":"true","avatar.source":"\"uploaded\"","avatar.content_type":"string — the sniffed type that was stored","avatar.size_bytes":"number","avatar.updated_at":"string — ISO timestamp"}},{"method":"DELETE","path":"/internal/admin/users/:userId/avatar","description":"Remove any user's uploaded avatar; resolution falls back to the provider URL or the generated image. Idempotent. Audit-logged as user.avatar_deleted against the user's domain. Rate-limited per user (30/hour).","auth":"admin superuser bearer token","response":{"ok":"true"}},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId/avatar","description":"Image bytes for a team's (company) avatar. Readable by any ACTIVE member of the organisation, the same visibility as GET /org/organisations/:orgId/teams/:teamId.","auth":"domain hash bearer token + access token (X-UOA-Access-Token header) + signed config","query":{"domain":"string (required)","config_url":"string (required) — same verified config as every other /org/* route","style?":"string — tiles | waves | rings | mono; overrides the config default and the per-user pick. Only affects generated avatars.","size?":"number — generated SVG width/height, clamped 16-512 (default 128). The viewBox is constant; ignored for raster images."},"response":{"content-type":"image/svg+xml; charset=utf-8 for generated avatars, or the stored/sniffed raster type (image/png | image/jpeg | image/webp) for uploaded and proxied provider images","X-UOA-Avatar-Source":"\"uploaded\" | \"provider\" | \"generated\" — where the returned bytes came from","Cache-Control":"private, max-age=300 (uploaded/proxied) or private, max-age=86400 (generated, deterministic)","ETag":"SHA-256 of the returned bytes; a matching If-None-Match gets 304","X-Content-Type-Options":"nosniff","Content-Disposition":"inline; filename=\"avatar.<ext>\"","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline' — SVG responses only","body":"raw image bytes — avatar GETs never return JSON"},"notes":"Team (\"company\") avatars mirror user avatars exactly (Docs/Auth/avatars.md §11). Precedence: uploaded team image → server-side proxy of the team icon_url → deterministic generated SVG seeded from the team id. The icon_url fetch is HTTPS-only, SSRF-guarded, ~5s/5 MiB capped, and any failure silently falls back to the generated image, so a known team always yields 200 with an image; X-UOA-Avatar-Source reports \"provider\" for a proxied icon_url. The team icon_url column itself is untouched by these endpoints."},{"method":"PUT","path":"/org/organisations/:orgId/teams/:teamId/avatar","description":"Set a team's uploaded avatar from a multipart upload. Requires the teams.manage capability — the same authorization as PUT /org/organisations/:orgId/teams/:teamId, i.e. an org OR team owner/admin under the default grant table. Rate-limited per org+team (30/hour).","auth":"domain hash bearer token + access token (X-UOA-Access-Token header) + signed config","query":{"domain":"string (required)","config_url":"string (required)"},"body":{"file":"multipart/form-data file part — PNG, JPEG or WebP, max 1 MiB. The type is decided by magic-byte sniffing, not the supplied mimetype; SVG and any non-raster upload is rejected with a generic error."},"response":{"ok":"true","avatar.source":"\"uploaded\"","avatar.content_type":"string — the sniffed type that was stored","avatar.size_bytes":"number","avatar.updated_at":"string — ISO timestamp"},"notes":"Two management paths, pick by the credential you hold: use /domain/teams/:teamId/avatar from a product backend (domain hash bearer only — consuming products keep a bound refresh credential rather than a spendable end-user access token, so the dual-auth /org routes cannot be driven from a backend at all), and the /org/organisations/:orgId/teams/:teamId/avatar routes only when the caller actually holds a live user access token, in which case org owner/admin is enforced."},{"method":"DELETE","path":"/org/organisations/:orgId/teams/:teamId/avatar","description":"Remove a team's uploaded avatar; resolution falls back to the team icon_url or the generated image. Idempotent. Requires the teams.manage capability, the same gate as the PUT. Rate-limited per org+team (30/hour).","auth":"domain hash bearer token + access token (X-UOA-Access-Token header) + signed config","query":{"domain":"string (required)","config_url":"string (required)"},"response":{"ok":"true"},"notes":"Two management paths, pick by the credential you hold: use /domain/teams/:teamId/avatar from a product backend (domain hash bearer only — consuming products keep a bound refresh credential rather than a spendable end-user access token, so the dual-auth /org routes cannot be driven from a backend at all), and the /org/organisations/:orgId/teams/:teamId/avatar routes only when the caller actually holds a live user access token, in which case org owner/admin is enforced."},{"method":"GET","path":"/teams/:teamId/avatar","description":"Image bytes for a team (team) avatar with no credential at all — the only unauthenticated avatar route, so a browser or native client can render it directly. Responses carry Cross-Origin-Resource-Policy: cross-origin; credentialed avatar routes keep the default same-origin policy. This is what the auth-window team chooser and GET /org/me team directory use. Not an existence oracle: an unknown or deleted team id renders the same deterministic generated SVG a real team with no image gets, so every id answers 200 with an image. Rate-limited per IP (300/hour) plus a global ceiling of 120,000/hour service-wide.","auth":"none (public)","query":{"style?":"string — tiles | waves | rings | mono; overrides the config default and the per-user pick. Only affects generated avatars.","size?":"number — generated SVG width/height, clamped 16-512 (default 128). The viewBox is constant; ignored for raster images."},"response":{"content-type":"image/svg+xml; charset=utf-8 for generated avatars, or the stored/sniffed raster type (image/png | image/jpeg | image/webp) for uploaded and proxied provider images","X-UOA-Avatar-Source":"\"uploaded\" | \"provider\" | \"generated\" — where the returned bytes came from","Cache-Control":"private, max-age=300 (uploaded/proxied) or private, max-age=86400 (generated, deterministic)","ETag":"SHA-256 of the returned bytes; a matching If-None-Match gets 304","X-Content-Type-Options":"nosniff","Content-Disposition":"inline; filename=\"avatar.<ext>\"","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline' — SVG responses only","body":"raw image bytes — avatar GETs never return JSON"},"notes":"Team (\"company\") avatars mirror user avatars exactly (Docs/Auth/avatars.md §11). Precedence: uploaded team image → server-side proxy of the team icon_url → deterministic generated SVG seeded from the team id. The icon_url fetch is HTTPS-only, SSRF-guarded, ~5s/5 MiB capped, and any failure silently falls back to the generated image, so a known team always yields 200 with an image; X-UOA-Avatar-Source reports \"provider\" for a proxied icon_url. The team icon_url column itself is untouched by these endpoints. No config_url is accepted here, so the generated style is always the platform default rather than a domain's avatars.default_style — an anonymous caller must not be able to aim the config fetcher."},{"method":"GET","path":"/domain/teams/:teamId/avatar","description":"Image bytes for a team's (company) avatar with no end-user context — for backend rendering. The team's organisation must belong to the authenticated domain; anything else is the standard generic 404.","auth":"domain hash bearer token","query":{"domain":"string (required)","config_url?":"string — optional; when supplied the signed config is fetched and verified AFTER the bearer check and its avatars.default_style is applied. Its domain claim must match ?domain=. An unauthenticated request is rejected before any fetch is attempted.","style?":"string — tiles | waves | rings | mono; overrides the config default and the per-user pick. Only affects generated avatars.","size?":"number — generated SVG width/height, clamped 16-512 (default 128). The viewBox is constant; ignored for raster images."},"response":{"content-type":"image/svg+xml; charset=utf-8 for generated avatars, or the stored/sniffed raster type (image/png | image/jpeg | image/webp) for uploaded and proxied provider images","X-UOA-Avatar-Source":"\"uploaded\" | \"provider\" | \"generated\" — where the returned bytes came from","Cache-Control":"private, max-age=300 (uploaded/proxied) or private, max-age=86400 (generated, deterministic)","ETag":"SHA-256 of the returned bytes; a matching If-None-Match gets 304","X-Content-Type-Options":"nosniff","Content-Disposition":"inline; filename=\"avatar.<ext>\"","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline' — SVG responses only","body":"raw image bytes — avatar GETs never return JSON"},"notes":"Team (\"company\") avatars mirror user avatars exactly (Docs/Auth/avatars.md §11). Precedence: uploaded team image → server-side proxy of the team icon_url → deterministic generated SVG seeded from the team id. The icon_url fetch is HTTPS-only, SSRF-guarded, ~5s/5 MiB capped, and any failure silently falls back to the generated image, so a known team always yields 200 with an image; X-UOA-Avatar-Source reports \"provider\" for a proxied icon_url. The team icon_url column itself is untouched by these endpoints. Two management paths, pick by the credential you hold: use /domain/teams/:teamId/avatar from a product backend (domain hash bearer only — consuming products keep a bound refresh credential rather than a spendable end-user access token, so the dual-auth /org routes cannot be driven from a backend at all), and the /org/organisations/:orgId/teams/:teamId/avatar routes only when the caller actually holds a live user access token, in which case org owner/admin is enforced."},{"method":"PUT","path":"/domain/teams/:teamId/avatar","description":"Set a team's uploaded avatar from a multipart upload, with no end-user context — the management path for product backends. No role check: per brief §24.10 the domain hash token is full system trust for that domain, and your backend enforces its own owner/admin gating before relaying. Audit-logged as domain.team_avatar_updated against the acting domain. Rate-limited per domain+team (30/hour).","auth":"domain hash bearer token","query":{"domain":"string (required)"},"body":{"file":"multipart/form-data file part — PNG, JPEG or WebP, max 1 MiB. The type is decided by magic-byte sniffing, not the supplied mimetype; SVG and any non-raster upload is rejected with a generic error."},"response":{"ok":"true","avatar.source":"\"uploaded\"","avatar.content_type":"string — the sniffed type that was stored","avatar.size_bytes":"number","avatar.updated_at":"string — ISO timestamp"},"notes":"Two management paths, pick by the credential you hold: use /domain/teams/:teamId/avatar from a product backend (domain hash bearer only — consuming products keep a bound refresh credential rather than a spendable end-user access token, so the dual-auth /org routes cannot be driven from a backend at all), and the /org/organisations/:orgId/teams/:teamId/avatar routes only when the caller actually holds a live user access token, in which case org owner/admin is enforced."},{"method":"DELETE","path":"/domain/teams/:teamId/avatar","description":"Remove a team's uploaded avatar; resolution falls back to the team icon_url or the generated image. Idempotent. Same full-trust domain-hash semantics as the PUT. Audit-logged as domain.team_avatar_deleted against the acting domain. Rate-limited per domain+team (30/hour).","auth":"domain hash bearer token","query":{"domain":"string (required)"},"response":{"ok":"true"},"notes":"Two management paths, pick by the credential you hold: use /domain/teams/:teamId/avatar from a product backend (domain hash bearer only — consuming products keep a bound refresh credential rather than a spendable end-user access token, so the dual-auth /org routes cannot be driven from a backend at all), and the /org/organisations/:orgId/teams/:teamId/avatar routes only when the caller actually holds a live user access token, in which case org owner/admin is enforced."},{"method":"GET","path":"/internal/admin/teams/:teamId/avatar","description":"Image bytes for any team, for the admin panel. Same resolution pipeline.","auth":"admin superuser bearer token","query":{"style?":"string — tiles | waves | rings | mono; overrides the config default and the per-user pick. Only affects generated avatars.","size?":"number — generated SVG width/height, clamped 16-512 (default 128). The viewBox is constant; ignored for raster images."},"response":{"content-type":"image/svg+xml; charset=utf-8 for generated avatars, or the stored/sniffed raster type (image/png | image/jpeg | image/webp) for uploaded and proxied provider images","X-UOA-Avatar-Source":"\"uploaded\" | \"provider\" | \"generated\" — where the returned bytes came from","Cache-Control":"private, max-age=300 (uploaded/proxied) or private, max-age=86400 (generated, deterministic)","ETag":"SHA-256 of the returned bytes; a matching If-None-Match gets 304","X-Content-Type-Options":"nosniff","Content-Disposition":"inline; filename=\"avatar.<ext>\"","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline' — SVG responses only","body":"raw image bytes — avatar GETs never return JSON"},"notes":"Team (\"company\") avatars mirror user avatars exactly (Docs/Auth/avatars.md §11). Precedence: uploaded team image → server-side proxy of the team icon_url → deterministic generated SVG seeded from the team id. The icon_url fetch is HTTPS-only, SSRF-guarded, ~5s/5 MiB capped, and any failure silently falls back to the generated image, so a known team always yields 200 with an image; X-UOA-Avatar-Source reports \"provider\" for a proxied icon_url. The team icon_url column itself is untouched by these endpoints."},{"method":"PUT","path":"/internal/admin/teams/:teamId/avatar","description":"Set any team's uploaded avatar from a multipart upload, for the admin panel. Audit-logged as team.avatar_updated against the owning domain. Rate-limited per team (30/hour).","auth":"admin superuser bearer token","body":{"file":"multipart/form-data file part — PNG, JPEG or WebP, max 1 MiB. The type is decided by magic-byte sniffing, not the supplied mimetype; SVG and any non-raster upload is rejected with a generic error."},"response":{"ok":"true","avatar.source":"\"uploaded\"","avatar.content_type":"string — the sniffed type that was stored","avatar.size_bytes":"number","avatar.updated_at":"string — ISO timestamp"}},{"method":"DELETE","path":"/internal/admin/teams/:teamId/avatar","description":"Remove any team's uploaded avatar. Idempotent. Audit-logged as team.avatar_deleted against the owning domain. Rate-limited per team (30/hour).","auth":"admin superuser bearer token","response":{"ok":"true"}},{"method":"GET","path":"/settings/me","description":"Every settings namespace of the access-token subject, plus quota usage.","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"response":{"ok":"true","namespaces":"object — { [namespace]: { [key]: JSON value } }","usage.entries":"number — keys stored across all namespaces","usage.size_bytes":"number — serialized UTF-8 bytes stored across all namespaces","usage.max_entries":"number — 500","usage.max_size_bytes":"number — 1048576"},"notes":"User settings (Docs/Auth/user-settings.md) are optional, user-owned JSON stored in namespaced segments: namespace → key → any JSON value (object, array, string, number, boolean). The acting identity is always the access-token subject. Settings belong to the user row, so a \"global\"-scope user sees the same settings from every product they sign into (that is the point: e.g. a shared \"global\" namespace), while a \"per_domain\"-scope user has a separate store per domain. Any product the user signs into can read and write every namespace — never store secrets here. Quotas: 256 KiB per value, 500 keys and 1 MiB total per user. Responses are Cache-Control: no-store."},{"method":"GET","path":"/settings/me/:namespace","description":"All keys in one of the caller's settings namespaces.","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"response":{"ok":"true","namespace":"string","settings":"object — { [key]: JSON value }; {} for an empty or unknown namespace","updated_at":"string | null — ISO timestamp of the most recent write in the namespace"},"notes":":namespace is 1-64 chars of lowercase a-z, 0-9, \"_\", \".\" or \"-\"; :key is 1-128 chars of A-Z, a-z, 0-9, \"_\", \".\" or \"-\". Both must start with a letter or digit."},{"method":"PATCH","path":"/settings/me/:namespace","description":"Atomically upsert and/or delete several keys in one namespace. Each value replaces the stored one whole (no deep merge); null deletes that key. Returns the namespace after the write. Rate-limited per domain+user (600/hour, shared by all settings writes).","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"body":{"settings":"object (required) — 1-100 entries { [key]: JSON value | null }; keys follow the :key format, values max 256 KiB serialized, max nesting depth 32"},"response":{"ok":"true","namespace":"string","settings":"object — { [key]: JSON value }; {} for an empty or unknown namespace","updated_at":"string | null — ISO timestamp of the most recent write in the namespace"},"notes":":namespace is 1-64 chars of lowercase a-z, 0-9, \"_\", \".\" or \"-\"; :key is 1-128 chars of A-Z, a-z, 0-9, \"_\", \".\" or \"-\". Both must start with a letter or digit. All-or-nothing: an invalid entry (400 INVALID_USER_SETTINGS / generic), an oversized value (413 SETTING_VALUE_TOO_LARGE) or a write that would exceed the per-user quota (413 SETTINGS_QUOTA_EXCEEDED) changes nothing. Strings containing NUL or unpaired surrogates and non-finite numbers are rejected."},{"method":"DELETE","path":"/settings/me/:namespace","description":"Remove a whole namespace. Idempotent. Rate-limited per domain+user (600/hour, shared by all settings writes).","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"response":{"ok":"true","deleted":"number — keys removed (0 when the namespace was empty)"}},{"method":"GET","path":"/settings/me/:namespace/:key","description":"One key of the caller's settings. A missing key is the standard generic 404.","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"response":{"ok":"true","namespace":"string","key":"string","value":"JSON value","updated_at":"string — ISO timestamp"}},{"method":"PUT","path":"/settings/me/:namespace/:key","description":"Create or replace one key. The value replaces the stored one whole — write a list such as bookmarks as the complete new list. Rate-limited per domain+user (600/hour, shared by all settings writes).","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"body":{"value":"JSON value (required, not null) — max 256 KiB serialized, max nesting depth 32. Use DELETE to remove a key."},"response":{"ok":"true","namespace":"string","key":"string","value":"JSON value — as stored","updated_at":"string — ISO timestamp of this write"},"notes":":namespace is 1-64 chars of lowercase a-z, 0-9, \"_\", \".\" or \"-\"; :key is 1-128 chars of A-Z, a-z, 0-9, \"_\", \".\" or \"-\". Both must start with a letter or digit. Same validation and quota errors as PATCH /settings/me/:namespace."},{"method":"DELETE","path":"/settings/me/:namespace/:key","description":"Remove one key. Idempotent. Rate-limited per domain+user (600/hour, shared by all settings writes).","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","query":{"domain":"string (required) — must equal the access token's domain claim"},"response":{"ok":"true"}},{"method":"GET","path":"/org/me","description":"Current user org context and complete authorized team directory","auth":"domain hash bearer token plus exactly one user credential: X-UOA-Access-Token or X-UOA-Subject-Assertion","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"response":{"system_admin":"boolean — live SUPERUSER on ADMIN_AUTH_DOMAIN for the authenticated ACTIVE subject with a current credential epoch; independent of org/team or product-domain roles. False without a database. Re-read for each privileged action; never persist or positively cache it. Responses are private/no-store.","org":"object | absent — live legacy org context plus the directory fields below. Normally anchored to the active same-domain organisation. For an `all_active_memberships` product whose selected team belongs to another domain, it is anchored to the token’s exact `active.orgId` after that organisation and the product policy are revalidated live. It remains absent when neither live context exists; an unscoped token never causes an arbitrary cross-domain org to be synthesized.","org.teams":"array of team IDs — the legacy claim field, identical in shape to the JWT `org.teams` claim and the keys of `org.team_roles`, but reflecting current database state. For the renderable directory use `org.team_directory`.","org.team_directory":"array — one entry per ACTIVE team membership on this domain, or every active membership when this product is explicitly mapped to `all_active_memberships`: { teamId, orgId, name, slug, orgName, orgSlug, iconUrl, avatarImageUrl, role, lastLoginAt }. Each entry carries its own orgId/orgName; do not assume it belongs to the singular legacy org.org_id. avatarImageUrl is the public /teams/:teamId/avatar form (never null, no credential needed). Entries are ordered lastLoginAt DESC with nulls last, then name ASC (the sidebar order). Cross-product entries have null `lastLoginAt`.","org.pending_invites":"array — every actionable invitation addressed to the caller across the organisations this product may show: { inviteId, orgId, orgName, orgSlug, teamId, teamName, invitedBy, expiresAt }. The reach is the directory's own — every organisation on this domain, plus every organisation the caller is an ACTIVE member of when this product is mapped to `all_active_memberships` — so an invitation into an organisation OTHER than the token's is listed here and must not be filed under the singular legacy org.org_id. orgId/orgName/orgSlug name the inviting organisation: label the card with them (two organisations can each own a team called \"General\") and select that organisation after acceptance. invitedBy is the inviter's NAME, and null when no name is known — never their e-mail address (an invitee is not told the sender's address anywhere else); render no inviter line when it is null. Ordered oldest invitation first."},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations","description":"List organisations for domain","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","limit?":"number — page size, max 200","cursor?":"string — id of the last row of the previous page (from next_cursor)"},"response":{"data":"array — organisation records","next_cursor":"string | null"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations","description":"Create organisation owned by the calling user (X-UOA-Access-Token). Non-superusers also require org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In backend mode (no user credential) the body must carry owner_user_id instead and allow_user_create_org does not apply. Without slug, the address is derived from name; a derived or chosen address another organisation on the domain already holds is refused with 400 ORG_SLUG_TAKEN (never suffixed), so ask the person for another name. The response is the organisation record plus defaultTeam: the first team, named after the organisation and created in the same transaction, with the owner already an ACTIVE member of it. Use defaultTeam.id to address the new team immediately — no follow-up read is needed, and none is possible with a subject assertion, which must already name the org and team it acts on.","auth":"domain hash bearer token + X-UOA-Access-Token header (the new org owner), or backend mode","body":{"name":"string (required, 1-100)","owner_user_id?":"string — backend mode only, and required there; rejected with 400 OWNER_NOT_ALLOWED alongside a user credential"},"response":{"…":"organisation record fields (id, domain, name, slug, ownerId, memberInvites, iconUrl, createdAt, updatedAt)","defaultTeam":"object — the full team record of the auto-created default team (isDefault true)"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId","description":"Get organisation details","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"PUT","path":"/org/organisations/:orgId","description":"Update organisation. User mode requires the organisation.manage capability at ORG scope (org_features.role_grants); team standing never authorises it, because administering a team confers nothing over the organisation containing it. Under the default grant table that is org owner/admin, unchanged.","auth":"domain hash bearer token","body":{"name":"string (optional)","member_invites?":"string — \"allowed\" (default) | \"admin_approval\" | \"disabled\"; same organisation.manage gate, omitted leaves it unchanged; gates the member-initiated invite endpoint","icon_url?":"string | null — external HTTPS URL only, max 2048 chars; same organisation.manage gate; omitted leaves the current icon unchanged, null clears it; non-https/oversized/invalid rejected with a generic error"},"response":{"iconUrl":"string | null — echoed on every organisation read/write"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"DELETE","path":"/org/organisations/:orgId","description":"Legacy direct organisation deletion returns 409 ENTITY_DELETION_WORKFLOW_REQUIRED. A platform administrator must preview and confirm a lifecycle deletion job.","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/members","description":"Stateless organisation roster. Every row is an exact organisation membership plus its scoped UOA identity. Email is returned only when the caller holds members.manage; permissions are action-specific verdicts, never a generic manager flag.","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","status?":"string — ACTIVE (default) | DEACTIVATED | REMOVED | all","limit?":"number — page size, max 200","direction?":"forward (default) | backward","cursor?":"opaque signed keyset cursor from meta.nextCursor or meta.prevCursor"},"response":{"data":"array — { id, orgId, subject, userId (legacy alias), role, status, identity { displayName, avatarImageUrl, email? }, avatarImageUrl, createdAt, updatedAt }; email is omitted without members.manage","total":"number — count for the selected status filter","meta":"object — { hasMore, nextCursor, prevCursor }","next_cursor":"string | null — compatibility alias for meta.nextCursor","permissions":"object — { addMember, changeMemberRole, removeMember, deactivateMember, reactivateMember, viewMemberEmail, transferOwnership, orgRoleOptions }; each is a live caller verdict. orgRoleOptions is the verified-config org role vocabulary excluding owner, present only when changeMemberRole is true; transferOwnership is true only for the ACTIVE canonical owner (or an explicitly authorized backend). Ownership transfer is a separate action."},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/members","description":"Add organisation member. User mode requires the members.manage capability at ORG scope (org_features.role_grants) — org owner/admin under the default table. The owner role is reserved for ownership transfer; ordinary membership operations cannot assign it or change an existing owner.","auth":"domain hash bearer token","body":{"userId":"string (required)","role":"string (optional, default \"member\") — validated against org_features.org_roles"},"response":{"avatarImageUrl":"string — the member's avatar image URL, fetchable with the same domain hash bearer"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/members/:userId/teams","description":"List the selected member's editable team access. The result contains only teams where the caller currently holds members.manage; it never exposes or changes memberships outside that scope.","auth":"domain hash bearer token","response":{"data":"array — { id, name, slug, avatarImageUrl, hasAccess }","permissions":"object — { changeTeamAccess }; live caller verdict"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"PUT","path":"/org/organisations/:orgId/members/:userId","description":"Change a non-owner member role. User mode requires members.manage at ORG scope, admitting owner/admin under the default table. The role is validated against org_features.org_roles; owner assignment and any change to an existing owner are refused.","auth":"domain hash bearer token","body":{"role":"string (required)"},"response":{"avatarImageUrl":"string — the member's avatar image URL, fetchable with the same domain hash bearer"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"DELETE","path":"/org/organisations/:orgId/members/:userId","description":"Remove organisation member (soft-remove: status becomes REMOVED; atomically revokes exact user+org refresh families across product domains plus legacy sessions on this domain). User mode requires the members.manage capability at ORG scope; removing an owner additionally requires the actor to BE an owner.","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/members/:userId/deactivate","description":"Deactivate an organisation member: suspends access and atomically revokes exact user+org refresh families across product domains plus legacy sessions on this domain; cannot deactivate an owner. Same members.manage gate as add/remove — deactivation is roster mutation, not a separate authority.","auth":"domain hash bearer token","response":{"ok":"true"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/members/:userId/reactivate","description":"Reactivate a DEACTIVATED organisation member (org + team rows return to ACTIVE); does not restore sessions — the user signs in again. Same members.manage gate as add/remove.","auth":"domain hash bearer token","response":{"ok":"true"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/transfer-ownership","description":"Transfer organisation ownership. Deliberately NOT a capability: the acting user must BE Organisation.ownerId.","auth":"domain hash bearer token","body":{"newOwnerId":"string (required) — alias: newOwnerUserId; send exactly one","previousOwnerRole?":"string — the org role the outgoing owner is left with. Validated against this domain org_features.org_roles like any other role write, and must not be \"owner\". Omitted: \"admin\" when the vocabulary contains it, else the first non-owner role in it; a vocabulary of only \"owner\" refuses the transfer."},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams","description":"List teams","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","limit?":"number — page size, max 200","cursor?":"string — id of the last row of the previous page (from next_cursor)"},"response":{"data":"array — team records including id, name, slug, description, groupId, isDefault, iconUrl and avatarImageUrl (the always-resolving team avatar image URL, fetchable with the same domain hash bearer — Docs/Auth/avatars.md §11)","next_cursor":"string | null"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams","description":"Create team. User mode requires the teams.manage capability at ORG scope — there is no team to stand in yet, so team-scope standing never authorises this. Under the default grant table that is org owner/admin, unchanged.","auth":"domain hash bearer token","body":{"name":"string (required)","slug?":"string — optional custom team slug; otherwise derived from name","description":"string (optional)","join_creator?":"boolean (default false) — add the acting user to the new team as its owner, in the same transaction. Set this when a person is creating their own team: every entry check asks for an ACTIVE TeamMember, so without it they create a team they cannot open. Leave it false when a backend is provisioning teams for other people. Idempotent (upsert), and ignored in backend mode, which has no acting user."},"response":{"slug":"string — unique team slug within the organisation"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId","description":"Get team details (includes members)","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","include?":"string — exact literal \"invited\" adds the invited[] array below; any other value is ignored (treated as absent)"},"response":{"slug":"string — unique team slug within the organisation","iconUrl":"string | null — echoed on every team read/write","avatarImageUrl":"string — always-resolving team avatar image URL (Docs/Auth/avatars.md §11), fetchable with the same domain hash bearer; never null, unlike iconUrl","members":"array — current team members; each carries avatarImageUrl, fetchable with the same domain hash bearer","invited?":"array — present only when include=invited: pending invites for this team, { inviteId, email, inviteName, teamRole, invitedByName, invitedByEmail, invitedByAvatarImageUrl, lastSentAt, expiresAt, approvalStatus, openCount }; gated to holders of the members.manage capability (invite emails are PII) — anyone else gets [] here, never a 403; absent entirely when ?include=invited is not passed"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"PUT","path":"/org/organisations/:orgId/teams/:teamId","description":"Update team. User mode requires the teams.manage capability, resolved over the union of the org-role and team-role grants (org_features.role_grants); under the default table that is an org OR team owner/admin.","auth":"domain hash bearer token","body":{"name":"string (optional)","slug?":"string — optional custom team slug; omitted leaves the current slug unchanged","description":"string (optional)","joinPolicy?":"string — INVITE_ONLY (default) | APPROVED_DOMAIN | REQUEST_TO_JOIN | OPEN_TO_ORG | HIDDEN; owner/admin only, omitted leaves the current policy unchanged","icon_url?":"string | null — external HTTPS URL only, max 2048 chars; owner/admin only; omitted leaves the current icon unchanged, null clears it; non-https/oversized/invalid rejected with a generic error"},"response":{"slug":"string — unique team slug within the organisation","iconUrl":"string | null — echoed on every team read/write","avatarImageUrl":"string — always-resolving team avatar image URL (Docs/Auth/avatars.md §11)"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"DELETE","path":"/org/organisations/:orgId/teams/:teamId","description":"Legacy direct team deletion returns 409 ENTITY_DELETION_WORKFLOW_REQUIRED. A platform administrator must preview and confirm a lifecycle deletion job, including any last-team organisation cascade.","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams/:teamId/join","description":"Self-join a team whose joinPolicy is OPEN_TO_ORG (caller must be an ACTIVE member of the team's org); reactivates a previously removed/deactivated membership instead of duplicating it","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","response":{"200":"team member record","400":"generic error — team not found, policy is not OPEN_TO_ORG, or already an active member"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/member-invitation-targets","description":"Cursor-paged explicit team chooser for the Members invitation dialog. It contains only teams in this exact organisation for which the caller currently holds members.manage; it never infers authority from a selected team or from the product session.","auth":"domain hash bearer token plus optional user credential; backend mode receives all teams","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","limit?":"number — page size, max 200","direction?":"forward (default) | backward","cursor?":"opaque signed keyset cursor from meta.nextCursor or meta.prevCursor"},"response":{"data":"array — { id, name, slug, avatarImageUrl }","total":"number — teams the caller may target with an invitation","meta":"object — { hasMore, nextCursor, prevCursor }","permissions":"object — { createInvitation }"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/member-invitations","description":"Organisation-wide cursor-paged feed of actionable TeamInvite records for the Members Pending invitations tab. This is not the owner-only member-invite approval queue: every row names its exact target team, and results are limited to teams where the caller currently holds members.manage.","auth":"domain hash bearer token plus optional user credential; backend mode receives all teams","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","limit?":"number — page size, max 200","direction?":"forward (default) | backward","cursor?":"opaque signed keyset cursor from meta.nextCursor or meta.prevCursor"},"response":{"data":"array — actionable invite records with status (pending|expired), full invitee email and target team { id, name, slug, avatarImageUrl }","total":"number — actionable invitation count for the permitted target teams","meta":"object — { hasMore, nextCursor, prevCursor }","permissions":"object — { createInvitation, viewPendingInvitations }"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId/member-invitations","description":"The exact-team form of the actionable Pending invitations feed for the team Members page. Its pagination envelope and row shape are identical to the organisation-wide feed; a caller without members.manage for this team receives no invite data.","auth":"domain hash bearer token plus optional user credential; backend mode receives the exact team","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","limit?":"number — page size, max 200","direction?":"forward (default) | backward","cursor?":"opaque signed keyset cursor from meta.nextCursor or meta.prevCursor"},"response":{"data":"array — actionable invite records with target team identity","total":"number — actionable invitation count for this exact team","meta":"object — { hasMore, nextCursor, prevCursor }","permissions":"object — { createInvitation, viewPendingInvitations }"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams/:teamId/invitations","description":"Dual-mode: with an X-UOA-Access-Token header, a single member-initiated invite gated by the org's member_invites setting (a members.manage holder — org/team owner/admin by default — always allowed; anyone else per setting; no email enumeration in the response). Without that header, the original trusted-backend bulk invite (unchanged).","auth":"domain hash bearer token; add access token (X-UOA-Access-Token header) for the member-initiated variant","body":{"redirectUrl?":"string — optional final OAuth redirect URL","invitedBy?":"object — backend-only variant: optional inviter metadata { userId?, name?, email? }","invites?":"array (backend-only variant, required, 1-200) — [{ email: string, name?: string, teamRole?: string }]","email?":"string — member-initiated variant (required instead of invites)","name?":"string — member-initiated variant","teamRole?":"string — member-initiated variant. Validated against org_features.team_roles (default owner|admin|member); owner is never invitable and is rejected with the generic 400. At most one actionable invite (not accepted/declined/revoked, approval not denied) exists per team + lowercased email — inviting an address that already holds one replaces it and reports resent_existing"},"response":{"results":"array (backend-only variant) — one object per submitted email: { email, status: invited | resent_existing | already_member | existing_user | conflict, invite? }. The invite key carries the full invitation record — including its id, for a later GET/resend/DELETE by id — on invited and resent_existing rows only; the three no-invitation outcomes carry email + status alone","status":"\"ok\" (member-initiated variant) — always the same shape regardless of outcome (no enumeration)"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId/invitations","description":"List invitation history for a team; user callers need current members.manage in this exact team.","auth":"domain hash bearer token plus optional access token or subject assertion; absent credentials require backend_org_management and origin-domain isolation","response":{"data":"array — invite records with status (pending|accepted|declined|replaced|revoked|expired), approvalStatus (not_required|pending|approved|denied), expiresAt, inviter, send/open, accepted/declined/revoked state, plus invitedByAvatarImageUrl and acceptedAvatarImageUrl (null until the matching user id exists; the invitee email never gets one)"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId/invitations/:inviteId","description":"Read one team invitation by id — the by-id companion to the invitation list, for a caller holding an id from a bulk-invite result, the list, or a resend. User callers need current members.manage in this exact team. Read-only: nothing is written and no audit row is produced.","auth":"domain hash bearer token plus optional access token or subject assertion; absent credentials require backend_org_management and origin-domain isolation","response":{"200":"invite record — exactly the shape the list returns per entry, including status (pending|accepted|declined|replaced|revoked|expired), approvalStatus (not_required|pending|approved|denied), expiresAt, inviter, send/open, accepted/declined/revoked state, invitedByAvatarImageUrl and acceptedAvatarImageUrl","404":"generic — unknown invite id, an invitation belonging to a foreign org/team, or cross-domain (no existence leak)"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams/:teamId/invitations/:inviteId/resend","description":"Resend a pending team invitation email; refreshes both the invite and emailed token to a complete 24-hour window. Only an actionable invitation with settled approval can be resent: accepted, declined, revoked, approval-denied, and still-awaiting-approval invitations all answer the generic 400, so a resend can neither resurrect a revoked invitation nor mail an unapproved one","auth":"domain hash bearer token plus optional access token or subject assertion; user callers need current members.manage in this exact team; absent credentials require backend_org_management and origin-domain isolation","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams/:teamId/invitations/:inviteId/accept","description":"Accept an exact team invitation for its invitee through backend mode. The product asserts the invitee's UOA user id; acceptance creates the ACTIVE org/team memberships and marks the invite accepted atomically. Repeating the exact accepted invite with the same userId is idempotent-success. The organisation's own origin domain is not compared with the calling product's: every client domain is equal and one organisation is usable from every product, so an invitation into an organisation founded through another product is accepted here. A person previously REMOVED from the organisation or team is re-admitted: the tombstoned rows return to ACTIVE with the default org role `member` (a role held before removal is never restored) and the invitation's teamRole, counted against member limits like a new membership and audited as member.reactivated / team_member.added (reactivated). A DEACTIVATED (suspended) org or team membership is not lifted by an invitation and is refused with MEMBERSHIP_DEACTIVATED; only POST .../members/:userId/reactivate lifts it. Unknown or mismatched ids, email mismatch, revoked/expired/unapproved invitations, and every other refusal remain generic.","auth":"backend mode only: domain hash bearer token with no X-UOA-Access-Token; requires org_features.backend_org_management=true","body":{"userId":"string (required, trimmed, non-empty) — UOA user id of the invitee asserted by the product backend"},"response":{"200":"{ ok: true, orgId, teamId }","400":"MEMBERSHIP_DEACTIVATED when the invitee is suspended in that organisation or team; generic for every other invalid or mismatched invitation state","401":"ACCESS_TOKEN_NOT_ALLOWED when X-UOA-Access-Token is present; MISSING_ACCESS_TOKEN when backend_org_management is not enabled"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"DELETE","path":"/org/organisations/:orgId/teams/:teamId/invitations/:inviteId","description":"Revoke a pending invitation (sent or still awaiting member-invite approval): the invite row is kept with status \"revoked\", outstanding emailed tokens are consumed, and the invite link refuses acceptance. Idempotent — revoking an already-revoked (or declined) invitation succeeds. User mode: a holder of the members.manage capability (org/team owner/admin under the default grant table) or the original inviter. Backend mode (header omitted): requires org_features.backend_org_management; audited with actor_user_id null + uoa_actor { via: \"domain_backend\" }.","auth":"domain hash bearer token; access token (X-UOA-Access-Token header) optional — omit for backend mode","response":{"200":"{ ok: true } (also for an already-revoked invitation)","403":"caller holds neither members.manage in this team nor authorship of the invitation","404":"generic — unknown invite id, foreign org/team, or cross-domain (no existence leak)","409":"code INVITATION_ALREADY_ACCEPTED — the invitation was already accepted; remove the member instead"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/invitations","description":"List invites awaiting member-invite approval for the organisation (requires ?approval=pending)","auth":"domain hash bearer token + access token (X-UOA-Access-Token header), owner/admin only","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","approval":"string (required) — must be \"pending\""},"response":{"data":"array — invite records with approvalStatus: pending, each carrying invitedByAvatarImageUrl and acceptedAvatarImageUrl (null until the matching user id exists)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/invitations/:inviteId/approve","description":"Approve a pending member-initiated invite: sets approvalStatus to approved and sends the invite email","auth":"domain hash bearer token + access token (X-UOA-Access-Token header), owner/admin only","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/invitations/:inviteId/deny","description":"Deny a pending member-initiated invite: sets approvalStatus to denied; sends nothing (silent to the invitee)","auth":"domain hash bearer token + access token (X-UOA-Access-Token header), owner/admin only","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams/:teamId/invite-links","description":"Create a shareable team invite link (Slack-style). Requires the members.manage capability (org/team owner/admin under the default grant table); refused (generic error) when the team's joinPolicy is HIDDEN. roleToAssign may be any role in org_features.team_roles except \"owner\", defaulting to \"member\". Returns the plaintext token ONCE; only its hash is stored.","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","body":{"roleToAssign?":"string — any role in org_features.team_roles except \"owner\"; defaults to \"member\"","maxUses?":"number — capped at 400 (default 400)","expiresInDays?":"number — capped at 30 (default 30)"},"response":{"token":"string — the plaintext invite-link token; shown only in this response","link":"object — { id, roleToAssign, expiresAt, maxUses, useCount, revokedAt, createdAt } (never the token)"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId/invite-links","description":"List invite links for a team (never includes the token itself). Requires the members.manage capability.","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","response":{"data":"array — { id, roleToAssign, expiresAt, maxUses, useCount, revokedAt, createdAt }"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"DELETE","path":"/org/organisations/:orgId/teams/:teamId/invite-links/:linkId","description":"Revoke a team invite link; idempotent (revoking an already-revoked link succeeds). Requires the members.manage capability.","auth":"domain hash bearer token + access token (X-UOA-Access-Token header)","response":{"revoked":"true"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId/access-requests","description":"List access requests for the configured team","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","status":"string (optional) — pending | approved | rejected"},"response":{"data":"array — access requests with requester, status, timestamps, reviewer metadata, plus avatarImageUrl for the requesting userId (null when the request has no user yet)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams/:teamId/access-requests/:requestId/approve","description":"Approve an access request and add the user to the configured team","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"body":{"reviewedByUserId":"string (optional) — reviewer user ID recorded with the approval","reviewReason":"string (optional, max 500) — free-form audit note"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams/:teamId/access-requests/:requestId/reject","description":"Reject an access request for the configured team","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"body":{"reviewedByUserId":"string (optional) — reviewer user ID recorded with the rejection","reviewReason":"string (optional, max 500) — free-form audit note"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId/members","description":"Stateless team roster. Supports lifecycle status filters and action-specific caller permissions. Email is returned only to members.manage holders (or the trusted backend in backend mode).","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","status?":"string — ACTIVE (default) | DEACTIVATED | REMOVED | all","limit?":"number — page size, max 200","direction?":"forward (default) | backward","cursor?":"opaque signed keyset cursor from meta.nextCursor or meta.prevCursor"},"response":{"data":"array — { id, teamId, subject, userId (legacy alias), role, teamRole, status, identity { displayName, avatarImageUrl, email? }, avatarImageUrl, createdAt, updatedAt }","total":"number — count for the selected status filter","meta":"object — { hasMore, nextCursor, prevCursor }","next_cursor":"string | null — compatibility alias for meta.nextCursor","permissions":"object — { addMember, changeMemberRole, removeMember, viewMemberEmail, searchMemberCandidates }; each is a live caller verdict"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/teams/:teamId/members/candidates","description":"Bounded, manager-only debounced candidate search for adding a member to this exact team. Searches ACTIVE members of this exact organisation only and excludes users already ACTIVE in the team; it is never a domain-wide user lookup.","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","q":"string (required, 1-100 characters) — server-side display-name or email match","limit?":"number — candidate count, default 20, max 50","direction?":"forward (default) | backward","cursor?":"opaque signed keyset cursor from meta.nextCursor or meta.prevCursor"},"response":{"data":"array — { subject, userId, orgRole, identity { displayName, avatarImageUrl, email }, avatarImageUrl }; email is safe here because members.manage is required","total":"number — count for this exact q and eligible-team filter","meta":"object — { hasMore, nextCursor, prevCursor }","permissions":"object — { addMember, searchMemberCandidates }"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/org/organisations/:orgId/teams/:teamId/members","description":"Add team member. User mode requires the members.manage capability, resolved over the union of the org-role and team-role grants (org_features.role_grants); under the default table that is an org OR team owner/admin — a team admin can administer their own roster without org standing.","auth":"domain hash bearer token","body":{"userId":"string (required)","teamRole":"string (optional, default \"member\") — validated against org_features.team_roles, this domain's configured team-role vocabulary"},"response":{"avatarImageUrl":"string — the member's avatar image URL, fetchable with the same domain hash bearer"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"PUT","path":"/org/organisations/:orgId/teams/:teamId/members/:userId","description":"Change team member role. Same members.manage gate as adding a member.","auth":"domain hash bearer token","body":{"team_role":"string (required) — validated against org_features.team_roles, this domain's configured team-role vocabulary"},"response":{"avatarImageUrl":"string — the member's avatar image URL, fetchable with the same domain hash bearer"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"DELETE","path":"/org/organisations/:orgId/teams/:teamId/members/:userId","description":"Remove team member (soft-remove; atomically revokes exact user+team refresh families across product domains without affecting other-team sessions). Same members.manage gate as adding a member.","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/groups","description":"List groups","auth":"domain hash bearer token","query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)","limit?":"number — page size, max 200","cursor?":"string — id of the last row of the previous page (from next_cursor)"},"response":{"data":"array — group records","next_cursor":"string | null"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/org/organisations/:orgId/groups/:groupId","description":"Get group details","auth":"domain hash bearer token","response":{"members":"array — group members; each carries avatarImageUrl, fetchable with the same domain hash bearer"},"query":{"domain":"string (required) — must match the config domain for domain-hash auth","config_url":"string (required)"},"notes":"Requires org_features.enabled=true (otherwise 404). Two calling modes. USER MODE: send exactly one of X-UOA-Access-Token or X-UOA-Subject-Assertion — the acting user is its `userId` claim, a new organisation is owned by that user (the body must not carry owner_user_id), and non-superusers can only create one when org_features.allow_user_create_org=true, else 403 ORG_CREATION_NOT_ALLOWED. In user mode an :orgId is resolved by ID ALONE: one organisation is usable from every UOA-integrated product, so the product that created it (organisations.domain, the origin domain) does not decide who may read or manage it. The gates are the token domain matching ?domain= (else 403 ACCESS_TOKEN_DOMAIN_MISMATCH), the token org claim matching :orgId (else 403 INSUFFICIENT_ORG_ROLE), and live ACTIVE membership/capability in the org. A subject assertion is a product-signed RS256 JWT, valid for at most 60 seconds, with audience https://authentication.unlikeotherai.com/org and an active org/team whose org exactly matches :orgId; the active team is caller provenance, not authority for a requested team. UOA verifies it through the product config JWKS and re-resolves the user credential epoch, ACTIVE org membership, and the requested team capability before applying the same user-mode gates. It is not an access token and must never be sent with X-UOA-Access-Token. Temporary product JWKS network, timeout, rate-limit or upstream-server failures return 503 INTEGRATION_JWKS_UNAVAILABLE; retry with backoff without ending the person’s session. Invalid signatures, missing keys, expired assertions and unsafe JWKS destinations remain 401 refusals. organisations.domain still owns the slug namespace, backend-mode ownership, and invite email identity. BACKEND MODE: omit X-UOA-Access-Token entirely — the domain pairing already on every /org route (domain-hash bearer + verified signed config + ?domain=) authorises the call, and there is no acting user, so per-user org/team role checks do not apply. Backend mode requires org_features.backend_org_management=true in the signed config; without it a missing token is still 401 MISSING_ACCESS_TOKEN. Backend mode never crosses domains: the call binds to the VERIFIED config domain (a differing ?domain= is 400 DOMAIN_MISMATCH) and an :orgId whose ORIGIN domain is not the verified one is 404 — backend mode is the one place that scoping still applies, because it has no acting user and therefore no membership to gate on. Where a route needs to name a user it takes one explicitly (owner_user_id on POST /org/organisations, userId on the member routes). GET /org/me and POST /org/organisations/:orgId/teams/:teamId/join are about the acting user and stay user-mode only (401 without a token). Backend-mode mutations are recorded in the org audit log with actor_user_id null and metadata.uoa_actor = { via: \"domain_backend\", source_domain }. GET /org/organisations is BACKEND-ONLY: it lists a whole domain and has no user mode, so it refuses either present user credential with 401 ACCESS_TOKEN_NOT_ALLOWED (valid or blank alike) — omit the header, and use GET /org/me for a user's own teams. Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/integrations/claim/:token","description":"One-time claim confirmation page for an approved integration request. Intentionally does NOT consume the token — link scanners cannot accidentally burn it.","auth":"public, no auth; token in path is a 32-byte random value hashed at rest","response":{"200":"HTML confirm page with a POST form targeting /integrations/claim/:token/confirm","404":"HTML invalid-link page when the token is missing, expired, used, or malformed"}},{"method":"POST","path":"/integrations/claim/:token/confirm","description":"Consumes the claim token and renders the one-time reveal page containing the partner client_secret and client_hash. Subsequent requests return the invalid-link page.","auth":"public; the token in the path IS the credential","response":{"200":"HTML reveal page: { domain, client_secret, client_hash, hash_prefix } shown once. Partner copies both into their backend secret store before closing.","404":"HTML invalid-link page when the token is missing, expired, used, or malformed"}},{"method":"POST","path":"/internal/admin/debug-login/issue","description":"Create or renew a single-use admin debug login","auth":"Current admin bearer and exact bearer-associated HttpOnly source cookie","query":{"config_url":"exact canonical first-party /internal/admin/config URL"},"body":{"previous_token":"optional previous one-use code to invalidate"},"response":{"200":"{url,token,expires_in}","429":"Rate limited","401/403":"Authentication failed"},"notes":"Exact service Origin and JSON POST required before config lookup. Current ACTIVE user, credential epoch, SUPERUSER admin-domain role and valid source family; secret domain credentials never accepted. At most 1800 seconds, capped by source session expiry. Clipboard JSON is exactly {url,token}."},{"method":"POST","path":"/internal/admin/debug-login/redeem","description":"Consume an admin debug code into an independent recipient family","auth":"One-use admin code; exact same-origin JSON POST","query":{"config_url":"exact canonical first-party /internal/admin/config URL"},"body":{"token":"opaque one-use code"},"response":{"200":"{access_token,expires_in,token_type:\"Bearer\"}; private Set-Cookie ownership handle","401":"Authentication failed"},"notes":"Exact service Origin and JSON POST required before config lookup. Current ACTIVE user, credential epoch, SUPERUSER admin-domain role and valid source family; secret domain credentials never accepted. Atomic single use; fresh short-lived admin bearer and independent private family. No refresh credential in JSON."},{"method":"POST","path":"/internal/admin/logout","description":"Revoke only the exact bearer-associated admin source family","auth":"Current admin bearer; exact same-origin JSON POST","query":{"config_url":"exact canonical first-party /internal/admin/config URL"},"body":{},"response":{"200":"{ok:true}"},"notes":"Clears its private cookie and invalidates unused source debug codes. Independent recipient/source sessions remain active."},{"method":"POST","path":"/internal/admin/users/:userId/teams","description":"Atomically add an existing user to a team, including organisation and default-team membership when needed","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"orgId":"string (required)","teamId":"string (required, belongs to orgId)","teamRole":"member | admin (required)"},"response":{"200":"{ ok: true, userId, orgId, teamId, teamRole }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Uses standard UOA membership limits. Exact retries are idempotent; existing active roles are preserved. Removed memberships may be re-added without restoring old privileges or sessions; deactivated memberships and owner changes are refused. Memberships and operator audit records commit together."},{"method":"GET","path":"/internal/admin/config","description":"Serve the signed first-party Admin config JWT used by /admin/login","auth":"Public, no-store. The returned JWT must verify through CONFIG_JWKS_URL and contain the admin domain, Google-only auth, and allow_registration=false.","response":{"200":"Signed RS256 config JWT as text/plain"}},{"method":"POST","path":"/internal/admin/token","description":"Browser-safe Admin UI authorization-code exchange; consumes the code and creates tokens in one BYPASSRLS transaction, and returns an admin access token only","auth":"Exact first-party config_url, service Origin and JSON POST; verified domain matches ADMIN_AUTH_DOMAIN; one-time authorization code with PKCE. Does not use domain-hash bearer auth and does not return refresh tokens.","body":{"code":"string (required)","redirect_url":"string (required)","code_verifier":"string (optional; required when the authorization code used PKCE)"},"query":{"config_url":"string (required)"},"response":{"200":"{ access_token, expires_in, token_type }; HttpOnly SameSite=Strict exact-bearer private session cookie"}},{"method":"GET","path":"/internal/admin/session","description":"Validate the current admin session access token","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ ok: true, adminUser: { id, email, domain, role, avatarImageUrl } }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/internal/admin/dashboard","description":"Admin dashboard aggregate data","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ stats, domains, organisations, users, logs, handshakeErrors, bans, apps }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/domains","description":"List domains and per-domain secret status known from the domain registry, roles, organisations, and logs","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"limit":"number (optional, max 200)"},"response":{"200":"Admin domain summary array","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains","description":"Register a domain and create its first domain client secret","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"domain":"string (required)","label":"string (optional)","client_secret":"string (optional, min 32); omitted means the API generates one"},"response":{"200":"{ domain, client_secret, client_hash, client_hash_prefix }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PUT","path":"/internal/admin/domains/:domain","description":"Update domain label, active/disabled status, 2FA policy, login access whitelist, or allowed redirect URLs","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"label":"string (optional)","status":"active | disabled (optional)","twoFaPolicy":"off | optional | required (optional)","allowed_email_domains":"string[] (optional, max 50)","allowed_emails":"string[] (optional, max 200)","allowed_redirect_urls":"string[] (optional, max 50) — redirect targets (https any host, http loopback-only, or native custom-scheme deep links) unioned into the domain config redirect_urls allowlist; empty clears the additions"},"response":{"200":"Updated domain registry row","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PATCH","path":"/internal/admin/domains/:domain","description":"Partially update domain label, active/disabled status, 2FA policy, login access whitelist, or allowed redirect URLs","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"label":"string (optional)","status":"active | disabled (optional)","twoFaPolicy":"off | optional | required (optional)","allowed_email_domains":"string[] (optional, max 50)","allowed_emails":"string[] (optional, max 200)","allowed_redirect_urls":"string[] (optional, max 50) — redirect targets unioned into the domain config redirect_urls allowlist; empty clears the additions"},"response":{"200":"Updated domain registry row","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/domains/:domain","description":"Get one domain with its organisations, teams, and users for directory browsing","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ domain, organisations, teams, users } or null — each users[] entry carries avatarSource and avatarImageUrl","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/internal/admin/domains/:domain/rotate-secret","description":"Rotate the domain client secret. deliveryMode=email (default) mints a 24h one-time claim link and emails it to the partner; the previous secret keeps working until the claim is consumed. deliveryMode=reveal returns the freshly minted credentials once in the response for out-of-band delivery and skips email. Self-supplied or generated secrets are prefixed with `uoa_sec_` and the hashing rule is still `sha256(domain + client_secret)` — prefix is part of the hashed input.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"client_secret":"string (optional, min 32); omit to let the API generate one. Generated secrets start with `uoa_sec_`.","deliveryMode":"\"email\" (default) or \"reveal\" — reveal returns credentials once and skips email"},"response":{"200":"{ domain, contact_email, delivery_mode, email_dispatched, hash_prefix, credentials? }. credentials = { domain, client_secret, client_hash, hash_prefix } is only present when delivery_mode=reveal and will never appear again.","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/domains/:domain/jwks","description":"List signing JWKs registered for a domain (active and deactivated)","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Array of { id, kid, fingerprint, active, created_at, deactivated_at, created_by_email }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains/:domain/jwks","description":"Add a public RSA JWK to a registered domain. Validator enforces kty=RSA, required kid/n/e, and rejects any private members.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"jwk":"object (required) — public RSA JWK with kty, kid, n, e"},"response":{"200":"Inserted { id, kid, fingerprint, active, created_at, deactivated_at, created_by_email }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/domains/:domain/jwks/:kid","description":"Soft-deactivate a domain JWK by kid. Config JWTs signed with this kid will stop verifying.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Deactivated { id, kid, fingerprint, active, created_at, deactivated_at, created_by_email }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/domains/:domain/email","description":"Read per-domain email sending configuration, live SES status, and DNS records","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ config, liveStatus, dnsRecords, adminCredentialsConfigured }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PUT","path":"/internal/admin/domains/:domain/email","description":"Create or update per-domain sender settings without calling SES","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"mailingDomain":"string (required)","fromAddress":"email (required; must end with @mailingDomain)","fromName":"string (optional)","replyToDefault":"email (optional)"},"response":{"200":"{ config }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains/:domain/email/register","description":"Register the configured mailing domain with SES and return TXT/DKIM DNS records","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ verification, dkim }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains/:domain/email/refresh","description":"Refresh SES verification and DKIM status for the configured mailing domain","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ verification, dkim, config }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PATCH","path":"/internal/admin/domains/:domain/email/enabled","description":"Enable or disable per-domain email sending; enabling requires SES verification and DKIM Success","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"enabled":"boolean (required)"},"response":{"200":"{ config }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/domains/:domain/email","description":"Delete local per-domain email config without removing the SES identity","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"204":"No content","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/confidential-delegations","description":"List the DB-backed per-product confidential token-exchange allowlist","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Array of { id, source_domain, product, resource, scopes, enabled, created_by_email, updated_by_email, created_at, updated_at }; never contains a client secret, domain hash, digest, or credential id","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/confidential-delegations","description":"Bind one registered source domain/product to one exact HTTPS resource and scope allowlist. The source app continues to authenticate with its own rotatable domain credential.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"source_domain":"string (required) — exact active ClientDomain whose existing domain credential authenticates this product","product":"lowercase identifier (required, [a-z0-9][a-z0-9._-]{0,99})","resource":"exact HTTPS resource URI (required; userinfo and fragments forbidden)","scopes":"non-empty unique array containing only \"ai.invoke\", \"billing.read\", \"token.provision\", \"memory.read\", \"memory.write\", and/or \"session:broker\" (broker scope only for approved Coder-to-Selkie binding); token provisioning is never implied by an AI grant","enabled":"boolean (optional; defaults true)"},"response":{"201":"{ id, source_domain, product, resource, scopes, enabled, created_by_email, updated_by_email, created_at, updated_at }; never contains a client secret, domain hash, digest, or credential id","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PATCH","path":"/internal/admin/confidential-delegations/:mappingId","description":"Change the exact resource, scope allowlist, or enabled state. Source domain and product are immutable; replace the mapping to rebind them.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"resource":"exact HTTPS resource URI (optional)","scopes":"non-empty unique array containing only \"ai.invoke\", \"billing.read\", \"token.provision\", \"memory.read\", \"memory.write\", and/or \"session:broker\" (broker scope only for approved Coder-to-Selkie binding) (optional)","enabled":"boolean (optional)"},"response":{"200":"{ id, source_domain, product, resource, scopes, enabled, created_by_email, updated_by_email, created_at, updated_at }; never contains a client secret, domain hash, digest, or credential id","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/confidential-delegations/:mappingId","description":"Delete a product delegation mapping; subsequent exchanges fail closed","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"204":"No content","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/domains/:domain/signatures","description":"Read signature settings, ordered agreements/versions, signature counts, and the latest 100 signature audit events","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ settings, agreements, audit_events }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PUT","path":"/internal/admin/domains/:domain/signatures/settings","description":"Enable/disable the optional domain signature gate and set explicit evidence retention. Enabling fails closed unless runtime storage, ClamAV, dedicated evidence keys, retention, and an active published required version are present.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"enabled":"boolean (required)","retention_days":"integer 1-36500 or null (required; null is forbidden while enabled)"},"response":{"200":"Updated settings with policy_revision","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains/:domain/signatures/agreements","description":"Create an agreement definition on a registered domain","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"title":"string (required, 1-200)","description":"string|null (optional, max 1000)","display_order":"integer (optional, 0-100000)","required_for_access":"boolean (optional, default true)"},"response":{"201":"Created agreement","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PUT","path":"/internal/admin/domains/:domain/signatures/agreements/:agreementId","description":"Update agreement metadata/order/required state. Published versions retain their immutable title snapshots.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"title":"string (required, 1-200)","description":"string|null (optional, max 1000)","display_order":"integer (optional, 0-100000)","required_for_access":"boolean (optional, default true)"},"response":{"200":"Updated agreement","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains/:domain/signatures/agreements/:agreementId/versions","description":"Upload one bounded PDF as a new draft version. Performs PDF structural/active-content checks, ClamAV scanning, exact SHA-256 hashing, private immutable storage, and audit.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"content_type":"multipart/form-data","file":"one application/pdf file (required; configured byte limit)","title":"string (required, 1-200)","signing_method":"clickwrap | typed_name (required)","acceptance_statement":"string (required, 1-4000; exact text shown and captured)"},"response":{"201":"Created draft version","429":"separate upload rate limit","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PUT","path":"/internal/admin/domains/:domain/signatures/agreements/:agreementId/versions/:versionId","description":"Edit signing metadata on a draft. Any published/superseded/withdrawn version is immutable.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"title":"string (required, 1-200)","signing_method":"clickwrap | typed_name (required)","acceptance_statement":"string (required, 1-4000; exact text shown and captured)"},"response":{"200":"Updated draft version","409":"immutable or invalid lifecycle transition","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PUT","path":"/internal/admin/domains/:domain/signatures/agreements/:agreementId/versions/:versionId/source","description":"Replace a draft source with one multipart application/pdf file; validates/scans/hashes/stores the replacement before retiring the previous draft object.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"content_type":"multipart/form-data","file":"one application/pdf file (required)"},"response":{"200":"Updated draft version","429":"separate upload rate limit","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains/:domain/signatures/agreements/:agreementId/versions/:versionId/publish","description":"Atomically publish a draft, supersede the prior published version, and increment domain policy_revision. Concurrent publication is serialized on the agreement row.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"effective_at":"ISO-8601 datetime (optional, default now)"},"response":{"200":"Published version","409":"not publishable","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains/:domain/signatures/agreements/:agreementId/versions/:versionId/withdraw","description":"Withdraw the currently published version without deleting evidence. A required version cannot be withdrawn while its domain gate is enabled.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Withdrawn version","409":"withdrawal would remove enabled requirement","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/domains/:domain/signatures/agreements/:agreementId/versions/:versionId","description":"Delete a draft version and its private draft object. Published/superseded/withdrawn versions cannot be deleted.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"204":"No content","409":"published evidence is immutable","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/domains/:domain/signatures/agreements/:agreementId/versions/:versionId/source","description":"Download the exact private source PDF with no-store/nosniff headers after domain/agreement/version scoping.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"application/pdf attachment; ETag contains immutable SHA-256","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/domains/:domain/signatures/records","description":"Search immutable signature evidence by signer email/name/reference, agreement, exact version, and signed date range. Results are always domain-scoped and omit the raw evidence JWS.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"q":"string (optional, max 320)","agreement_id":"string (optional)","agreement_version_id":"string (optional; takes precedence over agreement_id)","from":"ISO-8601 datetime (optional)","to":"ISO-8601 datetime (optional)","cursor":"signature id returned as next_cursor (optional; must belong to domain)","limit":"integer (optional, 1-100, default 50)"},"response":{"200":"{ data, next_cursor } — each row carries user_avatar_image_url for its user_id (the revocation actor_email has no user object and gets none)","429":"separate Admin signature-search limit","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/internal/admin/domains/:domain/signatures/records/:signatureId/receipt","description":"Download a private receipt after domain/ID scoping and SHA-256 verification. Every successful access writes both signature and global Admin audit events.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"application/pdf attachment with no-store/nosniff and hash ETag","429":"separate receipt-access limit","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/domains/:domain/signatures/records/:signatureId/revoke","description":"Append one immutable signature revocation. Retrying an already-revoked signature is idempotent and preserves the original revocation.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"reason":"string (required, 1-1000)"},"response":{"200":"{ id, signature_id, actor_email, reason, revoked_at }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/superusers","description":"List users with SUPERUSER on ADMIN_AUTH_DOMAIN","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Array of { userId, email, name, avatarImageUrl, createdAt }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/internal/admin/superusers/search","description":"Search non-superuser UOA users by email or name","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"q":"string (optional)"},"response":{"200":"Array of { userId, email, name, avatarImageUrl }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/internal/admin/superusers","description":"Grant SUPERUSER on ADMIN_AUTH_DOMAIN to an existing user","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"userId":"string (required)"},"response":{"201":"{ userId, email, name, avatarImageUrl, createdAt }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"DELETE","path":"/internal/admin/superusers/:userId","description":"Revoke SUPERUSER on ADMIN_AUTH_DOMAIN; refuses self-removal and last-superuser removal","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"204":"No content","409":"generic conflict for safety rails","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/integration-requests","description":"List auto-onboarding integration requests captured when unknown partner domains called /auth with jwks_url + contact_email in their config JWT.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"status":"PENDING | ACCEPTED | DECLINED (optional)","limit":"number (optional, max 200)"},"response":{"200":"Array of integration request summary rows","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/integration-requests/:id","description":"Get one integration request including public_jwk, config_summary, and pre_validation_result","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Integration request detail object or null","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/integration-requests/:id/accept","description":"Approve a pending integration request. Creates the ClientDomain, the first ClientDomainJwk, and a ClientDomainSecret in one transaction, then either emails a 24h one-time claim link to contact_email (deliveryMode=email) or returns the freshly minted credentials once in the response (deliveryMode=reveal).","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"label":"string (optional) — friendly label stored on the created ClientDomain","clientSecret":"string (optional, min 32) — omit to let the API generate one","deliveryMode":"\"email\" (default) or \"reveal\" — email dispatches a claim link; reveal returns credentials once and skips email"},"response":{"200":"Integration request detail (status=ACCEPTED) plus { delivery_mode, email_dispatched, credentials? }. credentials = { domain, client_secret, client_hash, hash_prefix } is only present when delivery_mode=reveal and will never appear again.","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/integration-requests/:id/decline","description":"Decline a pending integration request with a required internal reason. The partner is NOT emailed.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"reason":"string (required, max 1000) — internal audit reason"},"response":{"200":"Updated integration request detail (status=DECLINED)","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/integration-requests/:id/resend-claim","description":"Generate a fresh 24h claim token for an ACCEPTED request. Old token is revoked. deliveryMode=email (default) re-emails the claim link; deliveryMode=reveal returns the claim credentials once without sending an email.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"deliveryMode":"\"email\" (default) or \"reveal\""},"response":{"200":"Integration request detail plus { delivery_mode, email_dispatched, credentials? }. credentials = { domain, client_secret, client_hash, hash_prefix } is only present when delivery_mode=reveal and will never appear again.","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/integration-requests/:id","description":"Remove an integration request row. Any created ClientDomain is NOT deleted; this only clears the request record.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ ok: true }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/organisations","description":"List organisations with teams, members, and pre-approval rows","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"limit":"number (optional, max 200)"},"response":{"200":"Admin organisation summary array — owner and each members[] entry carry avatarImageUrl","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/internal/admin/organisations","description":"Create an organisation for an existing owner user; also creates the default General team","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"name":"string (required, max 100)","domain":"string (required)","owner_email":"email (required; must match an existing user)","allowed_email_domains":"string[] (optional, max 50)","allowed_emails":"string[] (optional, max 200)"},"response":{"200":"Created admin organisation object","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/organisations/:orgId","description":"Get one organisation with teams, members, and pre-approval rows","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Admin organisation object or null — owner and each members[] entry carry avatarImageUrl","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"PATCH","path":"/internal/admin/organisations/:orgId","description":"Replace organisation login access whitelist lists and/or 2FA policy","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"allowed_email_domains":"string[] (optional, max 50)","allowed_emails":"string[] (optional, max 200)","twoFaPolicy":"inherit | off | optional | required (optional)"},"response":{"200":"Updated admin organisation object or null","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/organisations/:orgId","description":"Legacy direct deletion is refused; use the platform-admin lifecycle preview, confirmation, and durable job workflow","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"409":"Generic error envelope with code ENTITY_DELETION_WORKFLOW_REQUIRED","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/organisations/:orgId/teams/:teamId","description":"Get one team with its parent organisation","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ org, team } or null","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PATCH","path":"/internal/admin/organisations/:orgId/teams/:teamId","description":"Update team name, description, and/or login access whitelist lists","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"name":"string (optional, max 100)","description":"string | null (optional, max 500)","allowed_email_domains":"string[] (optional, max 50)","allowed_emails":"string[] (optional, max 200)"},"response":{"200":"{ org, team } or null with updated team fields","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/teams","description":"List all teams with organisation context","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"limit":"number (optional, max 200)"},"response":{"200":"Admin team summary array; each entry carries avatarImageUrl (/internal/admin/teams/:teamId/avatar), fetchable with the same admin bearer — Docs/Auth/avatars.md §11","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/users","description":"List users with domains and latest login metadata","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"limit":"number (optional, max 200)"},"response":{"200":"Admin user summary array — each entry carries avatarSource and avatarImageUrl","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"GET","path":"/internal/admin/users/:userId","description":"Get one user summary","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Admin user summary object or null — carries avatarSource and avatarImageUrl","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"},"notes":"Avatar URLs in identity payloads (Docs/Auth/avatars.md §9): wherever a JSON response carries a UOA user's identity it also carries an absolute avatar image URL that always resolves to an image and is fetchable with the same credential class used for that request — <PUBLIC_BASE_URL>/domain/users/<userId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/users/<userId>/avatar in admin-bearer contexts. Both need a bearer credential, so fetch the URL with the credential you already hold and render the blob — a plain <img src> cannot call them. Bare actor-attribution emails with no user object, and invite rows for invitees who have no account yet, carry no avatar URL. Team records carry the same thing for the team itself: avatarImageUrl, <PUBLIC_BASE_URL>/domain/teams/<teamId>/avatar?domain=<domain> in domain-hash and dual-auth contexts, <PUBLIC_BASE_URL>/internal/admin/teams/<teamId>/avatar in admin-bearer contexts (except GET /org/me team_directory[], which use the public <PUBLIC_BASE_URL>/teams/<teamId>/avatar form so product-authorized cross-domain entries remain renderable; see Docs/Auth/avatars.md §11). It is derived and never null — unlike iconUrl, which keeps its existing \"externally hosted icon, may be null\" meaning."},{"method":"POST","path":"/internal/admin/users/:userId/2fa/disable","description":"Admin reset of a user 2FA enrollment; clears secret/counter, revokes refresh tokens, and writes AdminAuditLog","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Admin user summary object or null","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/logs","description":"List latest successful login events across domains or for one exact user ID; not a complete paginated history","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"limit":"number (optional, default 100, max 500)","userId":"string (optional, exact UOA subject)"},"response":{"200":"Login log array including userId (nullable), occurredAt (ISO UTC), legacy ts display, and result=ok","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/handshake-errors","description":"List sanitized app handshake and config JWT errors, including redacted request/response context for config fetch failures","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"limit":"number (optional, max 500)"},"response":{"200":"Sanitized handshake error log array with requestJson, responseJson, jwtHeader, jwtPayload, and redactions","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/bans","description":"List all admin bans grouped by type (emails, patterns, ips, users)","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ emails, patterns, ips, users } of { id, value, label, bannedAt, reason? }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/bans","description":"Create a ban. Scope is client-domain by default; supply org_id for an organisation ban or team_id for a team ban (the org/team must belong to domain). A ban overrides any allow-list and is enforced at login and registration.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"type":"string (required) — 'email' | 'pattern' | 'ip' | 'user'","value":"string (required) — email, glob pattern (e.g. *@evil.com), IP or CIDR, or userId","domain":"string (required) — client domain the ban is scoped to","org_id?":"string — organisation ban (must belong to domain)","team_id?":"string — team ban (must belong to domain); takes precedence over org_id","reason?":"string (max 500) — audit note"},"response":{"200":"{ id, value, label, bannedAt, reason? }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/bans/:id","description":"Remove a ban by id","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ id }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/api-keys","description":"List Admin API keys (no secret material). Each key controls feature flags and kill switches from a terminal/CI; minting and revoking is superuser-only.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"Array of { id, name, key_prefix, last_used_at, expires_at, revoked_at, created_by_email, created_at }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/api-keys","description":"Mint an Admin API key. The plaintext key (uoa_ak_…) is returned exactly once in the response and never shown again; only an HMAC digest is stored. The key can list apps and write feature flags + kill switches via X-API-Key auth — nothing else.","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"name":"string (required, max 120)","expires_at":"string (optional, ISO-8601 datetime; null or omitted means the key never expires)"},"response":{"201":"{ id, name, key_prefix, last_used_at, expires_at, revoked_at, created_by_email, created_at, key } — key is the one-time plaintext secret","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/api-keys/:id","description":"Revoke an Admin API key by id. The key stops authenticating immediately (sets revoked_at).","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"204":"No content","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/settings","description":"Admin settings backing data for bans and apps","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","response":{"200":"{ bans, apps }","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/apps","description":"List registered apps, each with its feature flag definitions and kill-switch rules. The discovery endpoint a terminal/CI caller uses to find the appId, flagId, and killSwitchId before writing flags or kill switches.","auth":"Superuser JWT (see adminAuth) OR an Admin API key sent as `X-API-Key: <key>` or `Authorization: Bearer uoa_ak_…`. The key is minted once by a superuser in the Admin panel and is scoped to exactly these routes (list apps + write feature flags + write kill switches); it cannot reach any other /internal/admin route or mint/list/revoke keys. When an API-key credential is present it is authoritative — an invalid/expired/revoked key returns 401 and never falls back to the JWT.","response":{"200":"Array of app summary objects (each with flags and kill switches)","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/apps","description":"Register an app for feature flags and startup payload lookups","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","body":{"name":"string (required, max 120)","identifier":"string (required, max 160; unique per organisation)","platform":"ios | android | web | macos | windows | linux | iot | tv | console | other","domain":"string (required)","org_id":"string (required)","offline_policy":"allow | block | cached (optional)","poll_interval_seconds":"number (optional, 30-86400)"},"response":{"200":"Created app summary object","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/apps/:appId/flags","description":"Create a feature flag definition for an app","auth":"Superuser JWT (see adminAuth) OR an Admin API key sent as `X-API-Key: <key>` or `Authorization: Bearer uoa_ak_…`. The key is minted once by a superuser in the Admin panel and is scoped to exactly these routes (list apps + write feature flags + write kill switches); it cannot reach any other /internal/admin route or mint/list/revoke keys. When an API-key credential is present it is authoritative — an invalid/expired/revoked key returns 401 and never falls back to the JWT.","body":{"key":"string (required, max 80)","description":"string (optional, max 500)","default_state":"boolean (required)"},"response":{"200":"Updated app summary object","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PATCH","path":"/internal/admin/apps/:appId/flags/:flagId","description":"Update a feature flag definition","auth":"Superuser JWT (see adminAuth) OR an Admin API key sent as `X-API-Key: <key>` or `Authorization: Bearer uoa_ak_…`. The key is minted once by a superuser in the Admin panel and is scoped to exactly these routes (list apps + write feature flags + write kill switches); it cannot reach any other /internal/admin route or mint/list/revoke keys. When an API-key credential is present it is authoritative — an invalid/expired/revoked key returns 401 and never falls back to the JWT.","body":{"key":"string (required, max 80)","description":"string (optional, max 500)","default_state":"boolean (required)"},"response":{"200":"Updated app summary object","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/apps/:appId/flags/:flagId","description":"Delete a feature flag definition and cascading flag values/overrides","auth":"Superuser JWT (see adminAuth) OR an Admin API key sent as `X-API-Key: <key>` or `Authorization: Bearer uoa_ak_…`. The key is minted once by a superuser in the Admin panel and is scoped to exactly these routes (list apps + write feature flags + write kill switches); it cannot reach any other /internal/admin route or mint/list/revoke keys. When an API-key credential is present it is authoritative — an invalid/expired/revoked key returns 401 and never falls back to the JWT.","response":{"200":"Updated app summary object","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"POST","path":"/internal/admin/apps/:appId/kill-switches","description":"Create a kill switch version rule for an app","auth":"Superuser JWT (see adminAuth) OR an Admin API key sent as `X-API-Key: <key>` or `Authorization: Bearer uoa_ak_…`. The key is minted once by a superuser in the Admin panel and is scoped to exactly these routes (list apps + write feature flags + write kill switches); it cannot reach any other /internal/admin route or mint/list/revoke keys. When an API-key credential is present it is authoritative — an invalid/expired/revoked key returns 401 and never falls back to the JWT.","body":{"name":"string (optional, max 120)","platform":"both | platform key","type":"hard | soft | info | maintenance","version_field":"versionName | versionCode | buildNumber","operator":"lt | lte | eq | gte | gt | range","version_value":"string (required, max 80)","version_max":"string (optional, max 80; required for range)","version_scheme":"semver | integer | date | custom","latest_version":"string (optional, max 80)","active":"boolean (required)","priority":"number (required, 0-1000)","cache_ttl":"number (optional, 60-86400)"},"response":{"200":"Updated app summary object","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"PATCH","path":"/internal/admin/apps/:appId/kill-switches/:killSwitchId","description":"Update a kill switch version rule","auth":"Superuser JWT (see adminAuth) OR an Admin API key sent as `X-API-Key: <key>` or `Authorization: Bearer uoa_ak_…`. The key is minted once by a superuser in the Admin panel and is scoped to exactly these routes (list apps + write feature flags + write kill switches); it cannot reach any other /internal/admin route or mint/list/revoke keys. When an API-key credential is present it is authoritative — an invalid/expired/revoked key returns 401 and never falls back to the JWT.","body":{"name":"string (optional, max 120)","platform":"both | platform key","type":"hard | soft | info | maintenance","version_field":"versionName | versionCode | buildNumber","operator":"lt | lte | eq | gte | gt | range","version_value":"string (required, max 80)","version_max":"string (optional, max 80; required for range)","version_scheme":"semver | integer | date | custom","latest_version":"string (optional, max 80)","active":"boolean (required)","priority":"number (required, 0-1000)","cache_ttl":"number (optional, 60-86400)"},"response":{"200":"Updated app summary object","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"DELETE","path":"/internal/admin/apps/:appId/kill-switches/:killSwitchId","description":"Delete a kill switch version rule","auth":"Superuser JWT (see adminAuth) OR an Admin API key sent as `X-API-Key: <key>` or `Authorization: Bearer uoa_ak_…`. The key is minted once by a superuser in the Admin panel and is scoped to exactly these routes (list apps + write feature flags + write kill switches); it cannot reach any other /internal/admin route or mint/list/revoke keys. When an API-key credential is present it is authoritative — an invalid/expired/revoked key returns 401 and never falls back to the JWT.","response":{"200":"Updated app summary object","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/search","description":"Search organisations, teams, and users","auth":"Authorization: Bearer <access_token>; admin tokens must be signed with ADMIN_ACCESS_TOKEN_SECRET, token role must be superuser, domain must match ADMIN_AUTH_DOMAIN, and DB-backed deployments require a SUPERUSER domain_roles row","query":{"q":"string (optional)"},"response":{"200":"Search result array for organisations, teams, and users","401/403":"401 when bearer token is missing/invalid; 403 when token is not an admin-domain superuser"}},{"method":"GET","path":"/internal/admin/billing/sms-policies/liabilities","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Page original inbound pending/uncollected/reconciliation receipts or outbound dispatching/uncertain/reconciliation holds.","query":{"kind":"inbound | outbound (required)","cursor":"optional last receipt/reservation ID"},"response":{"liabilities":"100 exact resource/allocation/org/team/account/message/dispatch bindings with customer credits; unknown credits remain null","next_cursor":"next cursor or null"},"notes":"No raw provider costs. Reviewing unknown or above-bound liability does not release holds, waive liability, debit credits or authorize a resend."},{"method":"POST","path":"/internal/admin/billing/sms-policies/recovery/:resourceId/verify-refund","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Verify already-existing full original Stripe cash refunds and a canceled subscription, then record immutable resource completion.","body":{"subscription_id":"exact original local subscription","refund_ids":"1–20 distinct existing re_ identifiers","reason":"8–500 character operator reason","verify_existing_refunds":"true"},"response":{"resource_id":"exact number resource","state":"ended after complete verified evidence","evidence_digest":"immutable SHA-256","refunded_at":"source balance transaction time"},"notes":"Reads Stripe only; never creates a refund or changes a subscription. Rechecks exact account/mode/resource/org/offer/subscription/invoice/cash/charge/refund/currency/full amount and canceled subscription; source-bound balance movement required. Partial, pending, foreign or changed evidence stays unresolved. Locked operator and resource recheck precede completion and audit. Exact retry is idempotent."},{"method":"GET","path":"/internal/admin/billing/sms-policies/recovery","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Read ending, recovery-required and refund-required number resources in bounded pages.","query":{"cursor":"optional exact last resource ID from previous page"},"response":{"resources":"100 private resource/org/product/phone/state/reason/time records","next_cursor":"next resource cursor or null"}},{"method":"GET","path":"/internal/admin/billing/sms-policies/recovery/:resourceId","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Review exact original customer quote and Stripe payment references for a number resource.","response":{"resource":"resource and provider binding; final customer quote; exact Stripe account/mode/subscription/initial invoice references","refund_action_available":"whether original paid references permit presenting the separate refund evidence form","operator_next_step":"explicit unresolved recovery instruction"},"notes":"No raw provider quote costs, markup or financial side effect. Reading never infers refund completion; only the explicit verified existing-refund action can end refund_required."},{"method":"GET","path":"/internal/admin/billing/sms-policies","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Read bounded immutable SMS policy acceptance history and open renewal alerts (private operator evidence).","response":{"fx":"latest 50 accepted FX snapshots, each with accepted_by operator | renewal_job (renewal_job has null accepted_by_user_id)","routes":"latest 100 accepted dimensional fee policies, each with accepted_by, provider_price (account price the bounds were accepted against; null = no automatic renewal) and renewal_of_id (the operator acceptance a renewal re-attests)","alerts":"up to 50 open renewal-job conditions needing a person: kind, subject (ECB policy or account:country:direction:currency), message, opened_at, last_seen_at"}},{"method":"GET","path":"/internal/admin/billing/sms-policies/monthly-prices","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Read the current flat monthly mobile-number price of each currency (EUR, GBP, USD), the same in every country.","response":{"prices":"one { id, amount (two places), currency, set_by: seed | operator, set_by_user_id, set_at } per currency"}},{"method":"POST","path":"/internal/admin/billing/sms-policies/monthly-prices","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Set one currency's new flat monthly mobile-number price; appends a row, never edits one.","body":{"amount":"two-place decimal above zero, e.g. 4.99","currency":"EUR | GBP | USD"},"response":{"price":"the new current price"},"notes":"An organisation's billing region picks the currency. New monthly quotes use it immediately; existing quotes and paid subscriptions keep their frozen price. A monthly quote is refused with 409 BILLING_SMS_MONTHLY_PRICE_BELOW_COST when the account rental, converted at the accepted ECB rates, is at or above it; that also opens a monthly_price_below_cost alert for that country and currency."},{"method":"POST","path":"/internal/admin/billing/sms-policies/fx/preview","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Refresh fixed ECB daily XML or validate its imported XML; no acceptance write.","body":{"xml":"optional bounded ECB daily XML; absent fetches the fixed source without redirects"},"response":{"evidence":"policy/source/SHA-256/rate date/observed time/expiry/USD-per-EUR exact decimal","preview_token":"operator-and-epoch-bound signed five-minute review","preview_expires_at":"ISO UTC"}},{"method":"POST","path":"/internal/admin/billing/sms-policies/fx/accept","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Explicitly accept the reviewed informational reference conversion as immutable commercial evidence.","body":{"preview_token":"exact fx preview","policy_understood":"true","acceptance_reason":"8–500 characters"},"response":{"policy":"immutable accepted evidence plus subject/time/reason"},"notes":"Source-date seven-day expiry cannot be extended by refresh or retry; altered, wrong-operator, expired or prior-epoch previews are refused."},{"method":"POST","path":"/internal/admin/billing/sms-policies/fx/refresh-accept","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Refresh and accept: fetch the fixed ECB daily XML and accept it under the operator subject with a generated reason when it lies within the renewal band of the last accepted rate.","body":{"policy_understood":"true"},"response":{"policy":"immutable accepted evidence plus subject/time/reason"},"notes":"Same band as the renewal job (BILLING_SMS_POLICY_AUTORENEW_FX_BAND_BPS). No accepted baseline: 409 BILLING_SMS_FX_BASELINE_REQUIRED; outside the band: 409 BILLING_SMS_FX_OUTSIDE_BAND; both leave the reviewed preview/accept flow for the operator. Source-date expiry is unchanged."},{"method":"POST","path":"/internal/admin/billing/sms-policies/routes/country/preview","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Add country: read the configured account Pricing API mobile prices for outbound and inbound, apply the versioned bound rule, and assemble and hash both evidence documents.","body":{"country":"uppercase ISO 3166-1 alpha-2 country"},"response":{"evidence":"country, configured account_sid, rule version, validity_days 30 and two route evidences (dimensions, bounds, source pricing page, digest, observed_at, expires_at, provider_price)","evidence_documents":"the two generated evidence texts, outbound first","preview_token":"signed five-minute review","preview_expires_at":"ISO UTC"},"notes":"No write. The account SID always comes from configuration. A non-USD price or an unpriceable route answers 409 BILLING_SMS_ROUTE_RULE_UNSUPPORTED or 503; use the manual route import instead."},{"method":"POST","path":"/internal/admin/billing/sms-policies/routes/country/accept","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Accept both reviewed Add country routes in one transaction under the operator subject with a generated reason.","body":{"preview_token":"exact country preview","policy_understood":"true","complete_segment_bound":"true","complete_message_bound":"true"},"response":{"routes":"the two immutable route policies, each recording provider_price as its automatic renewal baseline"}},{"method":"POST","path":"/internal/admin/billing/sms-policies/routes/preview","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Validate and hash a documented complete conservative route-fee bound for review.","body":{"account_sid":"exact AC SID","country":"uppercase ISO country","direction":"inbound | outbound","currency":"USD | EUR","additional_per_segment":"nonnegative Decimal(38,18) string","additional_per_message":"nonnegative Decimal(38,18) string","source":"8–500 character evidence reference","evidence":"20–32000 character fee document/explanation","expires_at":"future source-supported ISO timestamp"},"response":{"evidence":"exact dimensions, bounds, source, hash and expiry","preview_token":"signed five-minute review","preview_expires_at":"ISO UTC"}},{"method":"POST","path":"/internal/admin/billing/sms-policies/routes/accept","auth":"Current ADMIN_AUTH_DOMAIN SUPERUSER bearer; acceptance rechecks ACTIVE identity and credential epoch under billing administrator authority locks.","description":"Append reviewed private fee evidence after explicit completeness acceptance.","body":{"preview_token":"exact route preview","policy_understood":"true","complete_segment_bound":"true","complete_message_bound":"true","acceptance_reason":"8–500 characters"},"response":{"policy":"immutable dimensional policy plus subject/time/reason"},"notes":"Zero fees require documented supporting evidence; absence from Pricing API never proves zero. No provider or financial action. Preserve original source in the operator evidence archive."},{"method":"GET","path":"/.well-known/oauth-authorization-server","description":"RFC 8414 authorization-server metadata for the public-client / MCP profile","auth":"public; 404 unless MCP_OAUTH_PUBLIC_PROFILE_ENABLED=true and profile config is valid","response":{"issuer":"string","authorization_endpoint":"string","token_endpoint":"string","registration_endpoint":"string","revocation_endpoint":"string — RFC 7009 /oauth/revoke","jwks_uri":"string","grant_types_supported":"string[] — [\"authorization_code\", \"refresh_token\"]; refresh_token is issued only to registered native-app clients","code_challenge_methods_supported":"string[] — [\"S256\"]","token_endpoint_auth_methods_supported":"string[] — [\"none\"]","revocation_endpoint_auth_methods_supported":"string[] — [\"none\"]"}},{"method":"GET","path":"/oauth/jwks.json","description":"Public JWKS for verifying the RS256 tokens UOA issues: confidential resource tokens, optional public-profile tokens, and — when USER_ACCESS_TOKEN_* is configured — the user access token relying parties receive. Verify with algorithms=[\"RS256\"], iss=AUTH_SERVICE_IDENTIFIER, aud=\"uoa:access-token\"; kid separates the key classes. Signature verification is defence in depth and does not replace UOA as the authority on revocation (see Docs/Auth/access-token-verification.md). Separate from the config JWKS at /.well-known/jwks.json.","auth":"public when at least one RS256 signing key is configured; 404 otherwise","response":{"keys":"array — public RSA JWKs only"}},{"method":"POST","path":"/oauth/register","description":"RFC 7591 dynamic client registration (PUBLIC clients only; no secret issued)","auth":"public (IP rate-limited); 404 unless MCP_OAUTH_PUBLIC_PROFILE_ENABLED=true and profile config is valid","body":{"redirect_uris":"string[] (required; https / loopback http / native scheme)","client_name":"string (optional)","app_id":"optional admin-registered reverse-domain identifier; stored policy controls branding, callbacks and scopes","token_endpoint_auth_method":"string (optional; must be \"none\")","scope":"string (optional)"},"response":{"client_id":"string","redirect_uris":"string[]","grant_types":"string[] — [\"authorization_code\", \"refresh_token\"] when app_id resolved to an enabled native app; otherwise [\"authorization_code\"]","token_endpoint_auth_method":"none"}},{"method":"GET","path":"/oauth/authorize","description":"Authorization endpoint — validates client_id+redirect_uri+PKCE, renders the first-party login UI","auth":"public; 404 unless MCP_OAUTH_PUBLIC_PROFILE_ENABLED=true and profile config is valid","query":{"response_type":"code","client_id":"string (required)","redirect_uri":"string (required; must match a registered redirect URI)","code_challenge":"string (required; PKCE S256)","code_challenge_method":"S256 (required)","state":"string (optional)","scope":"string (optional; preserved through signing and bound to the authorization code)","resource":"string (optional; RFC 8707 — becomes the token aud)"},"response":{"200":"Login UI HTML"}},{"method":"POST","path":"/oauth/login","description":"Public email/password login (no secret); issues an auth code and returns the redirect target","auth":"public (IP rate-limited); 404 unless MCP_OAUTH_PUBLIC_PROFILE_ENABLED=true and profile config is valid","query":{"client_id":"string (required)","redirect_uri":"string (required)","code_challenge":"string (required; PKCE S256)","code_challenge_method":"S256 (required)","state":"string (optional)","scope":"string (optional; preserved exactly and cannot be widened at token exchange)","resource":"string (optional)"},"body":{"email":"string (required)","password":"string (required)","remember_me":"boolean (optional)","code":"six digit authenticator code (optional; resubmit password when twofa_required)","setup_token":"required with code when completing twofa_enroll_required"},"response":{"redirect_to":"string — redirect_uri?code=&state= (on success)","twofa_required":"boolean — resubmit password with authenticator code","twofa_enroll_required":"boolean — enroll using returned setup_token and manual_secret, then resubmit password, setup_token and code"}},{"method":"POST","path":"/oauth/token","description":"Public PKCE authorization-code exchange (no client secret) and, for registered native-app clients only, the rotating refresh-token grant; returns a resource-bound RS256 access token","auth":"public (PKCE or bound refresh token; separate IP rate-limit buckets for code and refresh grants); 404 unless MCP_OAUTH_PUBLIC_PROFILE_ENABLED=true and profile config is valid","body":{"grant_type":"authorization_code (optional, default) | refresh_token","code":"string (authorization_code: required)","redirect_uri":"string (authorization_code: required)","code_verifier":"string (authorization_code: required; PKCE)","refresh_token":"string (refresh_token: required; the latest opaque refresh token)","client_id":"string (required; a refresh token is bound to the exact client that obtained it)","scope":"string (optional; when supplied, must exactly match the originally granted scope — never widened or narrowed)"},"response":{"access_token":"string — RS256 JWT, aud = resource (or the issuer when none was granted)","token_type":"Bearer","expires_in":"number (seconds)","refresh_token":"string — native-app clients only: opaque, rotated on every refresh; persist the newest one before using the access token","refresh_token_expires_in":"number (seconds) — native-app clients only","scope":"string — refresh_token grant only: the originally granted scope (omitted when none was granted)"},"notes":"Refresh tokens are issued only to clients registered with an enabled native-app app_id; plain dynamic registrations are unchanged. Each refresh rotates the token within its family (REFRESH_TOKEN_TTL_DAYS lifetime, inherited on rotation). Re-presenting the immediate predecessor within 120 s returns the same live successor; a predecessor used later revokes the whole family and increments the user credential epoch. Every refresh re-checks, in one transaction, the client and its native-app policy, the user, the credential epoch the family was issued under (password reset, 2FA changes, logout anywhere or reuse revocation end it), the second-factor and signature policy, and the scope/resource allowlists, and never issues broader scope or resource than the original grant. Every refusal is 401 with the generic error body; malformed bodies are 400. Responses are Cache-Control: no-store."},{"method":"POST","path":"/oauth/revoke","description":"RFC 7009 revocation of a public-client refresh token; revokes its whole family when the token belongs to the presenting client","auth":"public (client_id binding; IP rate-limited); 404 unless MCP_OAUTH_PUBLIC_PROFILE_ENABLED=true and profile config is valid","body":{"token":"string (required; the refresh token)","client_id":"string (required; must be the client the token was issued to)","token_type_hint":"string (optional; ignored)"},"response":{"200":"{} — always, including unknown, foreign, repeated or malformed tokens"},"notes":"Same logout semantics as /auth/revoke: the family is revoked and the user credential epoch is incremented once, which invalidates current access tokens and ends every other public-client refresh family of that user. Cache-Control: no-store."},{"method":"POST","path":"/oauth/kelpie/pair","description":"Issue a short-lived same-account local pairing proof without exposing the account credential to the LAN.","auth":"Public OAuth RS256 bearer token, issuer audience, token_use=public_oauth, current credential epoch; public profile must be enabled","body":{"challenge":"43-character base64url nonce from the receiving Kelpie","clientId":"exact LAN client id, 1-200 characters"},"response":{"pairing_ticket":"profile-free RS256 at+jwt, token_use=kelpie_pair, challenge-bound audience","token_type":"Bearer","expires_in":"seconds, at most 120 and capped by issuing account expiry"},"notes":"Requires existing kelpie.connect consent on the enabled com.unlikeotherai.kelpie native app. Independent of gateway configuration. No new refresh grant, profile copy, or persistent pairing record. No-store, including errors."},{"method":"POST","path":"/oauth/kelpie/pair/validate","description":"Validate a LAN proof against the receiving Kelpie's own live UOA account.","auth":"Public OAuth RS256 bearer token, issuer audience, token_use=public_oauth, current credential epoch; public profile must be enabled","body":{"ticket":"same-account pairing proof","challenge":"exact receiving nonce","clientId":"exact LAN client id"},"response":{"active":"boolean","expires_at":"present only if active; epoch milliseconds, bounded by both accounts and proof expiry"},"notes":"Receiver requires profile scope and exact enabled Kelpie registration. Checks same subject, class/signature/audience/binding/expiry and both live authorities using current epoch, app revision, consent, factor, lifecycle, bans and login/signature policies. Invalid requester proof returns 200 {active:false}; only invalid receiver authority uses 401. Revalidate before every LAN/MCP request; fail closed on transport/server errors. No-store; no identity response."},{"method":"POST","path":"/oauth/kelpie/connect","description":"Issue a profile-free Nessie connection ticket for the current Kelpie native account.","auth":"Public OAuth RS256 bearer token, issuer audience, token_use=public_oauth, current credential epoch; public profile must be enabled","body":{"resource":"exact operator KELPIE_CONNECT_RESOURCE HTTPS URL"},"response":{"connection_ticket":"RS256 at+jwt, token_use=kelpie_connect; no profile or org","token_type":"Bearer","expires_in":"seconds, at most 120 and capped by account expiry","gateway_url":"corresponding WSS URL"},"notes":"Requires kelpie.connect scope and current enabled com.unlikeotherai.kelpie registration. No-store, including errors. Generic 401 invalid authority, 403 missing scope, 404 disabled, 400 invalid body/resource. Native keeps UOA account bearer local."},{"method":"POST","path":"/oauth/kelpie/connect/validate","description":"Live UOA authority validation for the configured Nessie gateway.","auth":"current domain hash bearer for the exact configured resource host","query":{"domain":"exact KELPIE_CONNECT_RESOURCE host"},"body":{"ticket":"Kelpie connection ticket"},"response":{"active":"true","iss":"UOA issuer","sub":"stable UOA subject","aud":"exact gateway resource","client_id":"native public client","app_id":"com.unlikeotherai.kelpie","exp":"ticket expiry seconds","jti":"ticket id"},"notes":"No profile or memberships. Rechecks signature, token class/audience/expiry, current registered app/client revision, server/client scopes, ACTIVE subject, credential epoch, factor, bans, login and signature policies and domain credential. Generic 401 invalid authority. Gateway revalidates at most every 30 seconds and before dispatch, closes on failure, reauthenticates before expiry. No-store, including errors."},{"method":"GET","path":"/oauth/me","description":"Current UOA subject, email and name. Profile scope required.","auth":"Public OAuth RS256 bearer token, issuer audience, token_use=public_oauth, current credential epoch; public profile must be enabled","response":{"sub":"string","email":"string","name":"string | null"}},{"method":"GET","path":"/oauth/me/avatar","description":"Current subject avatar, resolved by UOA. Profile scope required.","auth":"Public OAuth RS256 bearer token, issuer audience, token_use=public_oauth, current credential epoch; public profile must be enabled","response":{"200":"image bytes; no-store"}},{"method":"GET","path":"/oauth/me/settings/:namespace/:key","description":"Read an opaque personal setting. settings.read scope required.","auth":"Public OAuth RS256 bearer token, issuer audience, token_use=public_oauth, current credential epoch; public profile must be enabled","response":{"value":"JSON value or null when absent","ETag":"response header; opaque version for conditional writes"}},{"method":"PUT","path":"/oauth/me/settings/:namespace/:key","description":"Replace or delete one personal setting. settings.write scope required.","auth":"Public OAuth RS256 bearer token, issuer audience, token_use=public_oauth, current credential epoch; public profile must be enabled","body":{"value":"JSON value; null deletes"},"response":{"value":"saved JSON value or null"},"notes":"Requires If-Match with the ETag from GET. Missing/invalid precondition: 428; stale value: 409. Same store and quotas as /settings/me. Never supply a user id. No-store responses."},{"method":"POST","path":"/signatures/session","description":"Read the current missing agreements and receipts for a hosted signing continuation","auth":"short-lived opaque signing_token capability in the JSON body; token hash only is stored","body":{"signing_token":"string (required, 24-512)"},"response":{"complete":"boolean — current server-side policy result","policy_revision":"integer","agreements":"ordered current missing versions with exact title, description, method, acceptance statement, filename, and source SHA-256","receipts":"receipts created by this continuation, including verification reference/hash/state"},"notes":"Private no-store response. Invalid, expired, consumed, and exhausted capabilities fail identically."},{"method":"POST","path":"/signatures/session/source","description":"Stream the exact verified source PDF for a version currently required in this continuation","auth":"short-lived opaque signing_token capability in the JSON body; token hash only is stored","body":{"signing_token":"string (required)","agreement_version_id":"string (required)"},"response":{"200":"application/pdf inline; private no-store; SHA-256 ETag"}},{"method":"POST","path":"/signatures/session/sign","description":"Durably claim exact inputs, create deterministic evidence outside database locks, then revalidate and append one immutable signature for a currently missing version","auth":"short-lived opaque signing_token capability in the JSON body; token hash only is stored; same-origin browser mutation","body":{"signing_token":"string (required)","agreement_version_id":"string (required; revalidated server-side)","accepted":"boolean (required; must be true)","typed_name":"string|null (required only for typed_name; forbidden for clickwrap)"},"response":{"signature_id":"string","verification_reference":"non-guessable public reference","receipt_pdf_sha256":"lowercase SHA-256","session":"re-evaluated current session; retries are idempotent"}},{"method":"POST","path":"/signatures/session/receipt","description":"Download a hash-verified receipt created by this exact signing continuation","auth":"short-lived opaque signing_token capability in the JSON body; token hash only is stored","body":{"signing_token":"string (required)","signature_id":"string (required)"},"response":{"200":"application/pdf attachment; private no-store; SHA-256 ETag"}},{"method":"POST","path":"/signatures/session/complete","description":"Atomically re-check policy, consume the continuation once, issue the authorization code, and return the preserved redirect","auth":"short-lived opaque signing_token capability in the JSON body; token hash only is stored; same-origin browser mutation","body":{"signing_token":"string (required)"},"response":{"complete":"boolean","signatures_required":"true only when policy changed and more signing is required","redirect_to":"preserved validated OAuth/config-JWT continuation target"}},{"method":"GET","path":"/signatures/me/status","description":"Read current agreement completion for the access-token subject and token domain","auth":"X-UOA-Access-Token header","response":{"enabled":"boolean","complete":"boolean","policy_revision":"integer","requirements":"current ordered requirements and subject-owned current receipt references"}},{"method":"GET","path":"/signatures/me/receipts/:signatureId","description":"Download a hash-verified receipt restricted to the access-token subject and domain","auth":"X-UOA-Access-Token header","response":{"200":"application/pdf attachment; private no-store; SHA-256 ETag"}},{"method":"GET","path":"/signatures/verify/:reference","description":"Public PII-minimised verification of evidence JWS, source bytes, receipt bytes, and revocation state","auth":"public (IP rate-limited; non-guessable reference)","response":{"state":"valid | revoked","integrity_verified":"true","allowed_fields":"verification_reference, agreement/version IDs, version number, source/receipt hashes, signed_at, evidence_kid, revoked_at","pii":"never returns user ID, name, email, IP address, or user agent"}},{"method":"POST","path":"/domain/signatures/status","description":"Domain backend read of current agreement completion for one user on the verified domain","auth":"verified config_url + domain hash bearer token","query":{"config_url":"string (required)"},"body":{"user_id":"string (required; must have a role on the verified domain)"},"response":{"enabled":"boolean","complete":"boolean","policy_revision":"integer","requirements":"current ordered requirements and satisfaction evidence references"}}],"customer_invoices":"Actual prepaid payment and monthly service charge invoices use the /billing/v1/invoices/* endpoints; their canonical JSON Schema and examples are at /schemas/billing-customer-invoices-v1.json.","lifecycle":{"access_status_debug":"Disabled by default. AUTH_ACCESS_STATUS_ENABLED=true on the auth server enables the login diagnostic and all four website/native proof endpoints. When false or unset, every start/verify returns 404 before config/client lookup, mail or proof work, including outstanding proof redemption. Product config, DEBUG_ENABLED, URL parameters and browser storage cannot enable it.","states":["ACTIVE","DISABLED","DELETING","DELETED"],"deleted_identity":"{id,deleted:true,name:\"Deleted user\"}; PII null/omitted. The boolean derives from lifecycle state.","deletion_modes":"RETAIN_REFERENCE retains a non-authenticating subject; ERASE_REFERENCE erases mutable references. Immutable financial/signing evidence can require a restricted identity stub and is explicitly listed.","product_delivery":"Durable pull/ack, exact ClientDomain identity, exact revision, no arbitrary hooks. Missing registered participant authority or pending product acknowledgement blocks completion.","access":"Lifecycle denial has no superuser bypass. Parent disable inherits denial without changing independent child or membership states. Offline product JWT acceptance must use online introspection/revocation or bounded expiry.","legacy_delete":"Existing direct organisation/team DELETE routes return409 ENTITY_DELETION_WORKFLOW_REQUIRED; use platform-admin preview/confirmation/job flow."},"debug_login":{"issue":{"method":"POST","path":"/auth/debug-login/issue","auth":"domain bearer plus source refresh_token","body":{"refresh_token":"string","previous_token":"optional one-use token to invalidate"},"response":{"token":"opaque one-use code","expires_in":1800}},"redeem":{"method":"POST","path":"/auth/debug-login/redeem","auth":"domain bearer","body":{"token":"opaque one-use code"},"response":"Independent access and refresh token pair"},"binding":"Exact verified config_url, domain, client, source refresh family, credential epoch and selected team. Supported Nessie theme variants of the signed config URL are presentation-only; other query parameters retain their identity. Tokens travel only in POST bodies."},"admin_debug_login":{"issue":{"method":"POST","path":"/internal/admin/debug-login/issue","auth":"current admin bearer and exact bearer-bound HttpOnly source cookie; same-origin JSON POST","query":{"config_url":"exact first-party /internal/admin/config URL"},"body":{"previous_token":"optional previous code"},"response":{"url":"canonical /admin/login URL","token":"one-use code","expires_in":"seconds, capped by source lifetime"}},"redeem":{"method":"POST","path":"/internal/admin/debug-login/redeem","auth":"one-use admin code; same-origin JSON POST","query":{"config_url":"exact first-party admin config URL"},"body":{"token":"one-use code"},"response":{"access_token":"new short-lived admin bearer","expires_in":"seconds","token_type":"Bearer"}},"logout":{"method":"POST","path":"/internal/admin/logout","auth":"admin bearer; exact same-origin JSON POST","query":{"config_url":"exact first-party admin config URL"},"body":{},"response":{"ok":true}},"binding":"Current ACTIVE canonical identity, SUPERUSER admin-domain role, credential epoch, exact config and source family. A new private cookie is set for the independent recipient family; refresh tokens never appear in JSON."},"product_api_concurrency":"Product data APIs — /org/*, /domain/*, /settings/*, /internal/org/*, /avatar/*, /email/* — share a small per-instance concurrency cap so sign-in always keeps database connections. An excess request waits briefly in FIFO order, then answers 503 with Retry-After: 1 and code PRODUCT_API_BUSY (the request had no effect). Retry after the delay with backoff and never fan out one UOA call per end-user request: resolve /org/me and settings once per session and cache them. /auth/* (including /auth/token), /oauth/*, /2fa/*, /integrations/*, /internal/admin/*, /billing/* and discovery are never limited.","org_me_subject_assertion":"GET /org/me accepts either a UOA access token or a one-minute product-signed subject assertion and returns freshly resolved org roles, never product capability verdicts. Temporary source-JWKS DNS EAI_AGAIN returns 503 INTEGRATION_JWKS_UNAVAILABLE before identity admission; retain the session and retry with backoff.","team_invitation_management":"Team invitation history, detail and resend require current members.manage for the exact target team with a user credential; credential-free backend mode requires explicit opt-in and origin-domain isolation."}