This document answers one question: what request and response shape does every XID HTTP endpoint promise to callers, and how strong is the evidence behind that promise. It is written for integrators wiring up the Management API, the Hosted Auth endpoints, or SCIM, and for contributors changing those endpoints.
For protocol-level support depth read docs/protocols/; for how to call the SDKs read docs/sdks/.
The status column in the tables carries two distinct meanings; do not conflate them:
PASSmeans the contract has an implementation plus matching code or test evidence. It does not mean the path has been exercised in a real environment.- An entry marked L4 means the path has been verified in a real deployment environment.
Reading rules:
- An entry backed only by mock D1, mock fetch, mock queue, fake provider, fake browser, component tests, or focused unit tests counts as L1 only, and MUST NOT be used to claim production readiness.
- Full evidence for auth, cookies, route fallback, public docs, console, provider, D1, Queues, and deployment lives in the L2/L3/L4 columns of
docs/protocols/source-map.md,docs/protocols/gap-audit.md, anddocs/sdks/platform-matrix.md. - Even when a sign-in endpoint is
PASSat the unit-test level, it MUST still be verified with real Worker HTTP, a real browser, and a real deployment environment: after the cookie is written, a request to/v1/mecarrying that same cookie MUST succeed.
The public product model is Instance, Organization, Project, Application, User, Session. TenantContext, tenant_id, and the tenant query layer are internal resolver and data isolation implementation only, not product objects. User-visible UI, public docs, public API documentation, and SDK documentation MUST all use Organization.
The fixed contracts that follow from this:
- The public paths are
/v1/platform/organizationsand/scim/v2/organizations/{organization_id}./v1/platform/tenants,TenantItem, and/scim/v2/{tenant_id}are historical shapes, already removed, and requests return 404. POST /v1/sessions/active-organizationreturns a lightweightActiveOrganizationResponse; after a successful call the core SDK re-runsload()to read/v1/me. The request body MUST explicitly includeorganizationIdas a string or null; a missing field or an invalid value returns 422.- SDK constructor options MUST NOT accept
tenantand MUST NOT send anX-Xid-Tenantheader. The SDK accepts onlyapiUrlas the self-hosting or custom domain entry override; organization resolution is done by Host, OIDC context, identifier resolver, session, and the explicitorganizationIdAPI.
Authentication: public auth endpoints or a cookie session. The public product semantic is organization policy; internal configuration is read from TenantContext. Social provider configuration comes from the internal auth policy, and a third-party clientSecret is referenced through clientSecretRef pointing at a Workers Secret.
| Method | Path | Status | Response |
|---|---|---|---|
| GET | /auth/config |
production L4 | Publicly safe Hosted Auth configuration; provider secrets are never returned. Ambiguous matches expose public candidates through organizationId; tenant_id is only an internal resolver hint or a historical test parameter and MUST NOT appear in SDK constructor options or public documentation examples |
| POST | /auth/password/sign-in |
PASS | Unified password flow. An existing user that satisfies policy gets a session cookie; a new identifier that satisfies password.allowUserCreation creates the user and returns { nextStep: "verify_email" } or { nextStep: "complete" } |
| POST | /auth/sign-out |
PASS | Revokes the current active browser session, clears its refresh cookie and active-session pointer, and returns HTTP 200 { ok: true }; a request without a session returns the same idempotent response |
| POST | /auth/passkey/challenge |
PASS | passkey challenge |
| POST | /auth/passkey/verify |
PASS | { redirectUrl? }; on success writes the session cookie |
| POST | /auth/forgot-password |
PASS | enumeration-resistant success response |
| POST | /auth/reset-password |
PASS | refreshes the session view on success |
| POST | /auth/verify-email |
PASS | { ok: true } |
| POST | /auth/resend-verification |
PASS | enumeration-resistant success response |
| POST | /auth/magic-link/send |
PASS | enumeration-resistant success response |
| GET | /auth/magic-link/verify |
PASS | Mutation-free compatibility redirect from a legacy query-string token to the Hosted UI /magic-link#token=... confirmation page; a missing or unresolvable credential redirects to the tokenless branded error state instead of rendering API JSON; never consumes the token or writes a session |
| POST | /auth/magic-link/verify |
PASS | Explicit user-confirmed token consumption; on success writes the session cookie and returns { redirectUrl } |
| POST | /auth/otp/email/send |
PASS | enumeration-resistant success response |
| POST | /auth/otp/email/verify |
PASS | { redirectUrl? }; on success writes the session cookie |
| POST | /auth/otp/whatsapp/send |
PASS | When the provider is ready, writes the OTP token and enqueues it to WHATSAPP_QUEUE; when the provider is not configured, returns the enumeration-resistant response and writes a policy denial audit event |
| POST | /auth/otp/whatsapp/verify |
PASS | { redirectUrl? }; on success writes the session cookie |
| POST | /auth/otp/sms/send |
PASS | When the provider is ready, writes the OTP token and enqueues it to SMS_QUEUE; when the provider is not configured, returns the enumeration-resistant response and writes a policy denial audit event |
| POST | /auth/otp/sms/verify |
PASS | { redirectUrl? }; on success writes the session cookie |
| POST | /auth/mfa/sms/send |
PASS | When the provider is ready, enqueues to SMS_QUEUE; when the provider is not configured, rejects |
| POST | /auth/mfa/verify |
PASS | { redirectTo? } |
| GET | /auth/consent-params |
PASS | consent prompt payload |
| POST | /auth/consent |
PASS | OIDC authorize resume redirect |
| GET | /auth/:provider/authorize |
PASS | Upstream OAuth provider redirect |
| GET/POST | /auth/:provider/callback |
L1/L3 fake covered, real provider L4 missing | OAuth callback; on success writes the session cookie. A raw invitation capability is not accepted as social authentication input: an unauthenticated holder must complete /auth/invitation/claim and /auth/invitation/claim/verify before any later authenticated social linking. L1 covers the provider gate, the policy gate, root state tenant restore, and the default /console fallback not returning to the issuer root; the L3 fake social smoke test exists (apps/server/tests/smoke/l3-social-oauth.test.mjs); real OAuth provider end-to-end sign-in evidence is still missing |
| GET | /auth/social/:provider |
PASS | HTTP 404. The old social alias is no longer registered |
| POST | /auth/sign-up |
PASS | HTTP 404. User creation happens only through the unified /auth/password/sign-in policy branch |
| POST | /sso/hrd |
PASS | { connectionId, orgId, protocol } or { connectionId: null }, gated by the enterprise SSO policy and the domain policy |
Hosted Auth policy rules:
- The frontend
GET /auth/configreturns publicly safe fields only. - The UI shows only methods where
enabled = trueand either sign-in or user creation is allowed; whenforceSsomatches, only enterprise SSO is shown. - Frontend passkey Conditional UI MUST also be controlled by the same policy gate. When
/auth/configdoes not enable passkey, the sign-in page MUST NOT automatically request/auth/passkey/challenge. - A social provider that is unconfigured or disabled MUST NOT appear in
socialProviders. - The Worker re-validates policy for password, magic link, email OTP, WhatsApp OTP, SMS OTP, passkey, and social authorize/callback.
- Worker-side policy denials are all written to
AUDIT_QUEUEwith event nameauth.policy_denied. The payload containsmethod,action,reason,path,ip,identifierType,identifierHash,emailDomain, and optionalprovider; it MUST NOT contain a full email address, phone number, or plaintext OTP code. - After a successful send, the email, WhatsApp, and SMS consumers write to
AUDIT_QUEUEwith event namenotification.sent. The payload containschannel,type,provider,recipientType,recipientHash,emailDomain; it MUST NOT contain a full email address, phone number, token, or plaintext OTP code. A provider send failure is recorded innotification_failures, butrecipientstores onlysha256:<hash>, andpayloadstores only non-secret fields such as channel, type, tenantId, orgId, userId, locale, purpose, action, expiresInMin, recipientType, recipientHash, emailDomain; it MUST NOT contain a full email address, phone number, link, token, code, or message body. - The unified password endpoint distinguishes
loginfromuser_creationby whether the identifier already exists. An existing user MUST satisfypassword.allowLogin; a new user MUST satisfypassword.allowUserCreationand pass the identifier mode, domain, password length, and HIBP checks. - For new user creation the social callback MUST check
allowUserCreation,requireVerifiedEmail,allowedEmailDomains,blockedEmailDomains. - The Magic Link send endpoint distinguishes
loginfromuser_creationby whether the email already exists. An existing user MUST satisfymagicLink.allowLogin, a new user MUST satisfymagicLink.allowUserCreation; a policy denial still returns the enumeration-resistant success response and sends no mail. - The Magic Link JWT carries
action = login | user_creation; after consuming the token the verify endpoint re-validates policy against that action and marks the primary email as verified. - The Email OTP send endpoint distinguishes
loginfromuser_creationby whether the email already exists. An existing user MUST satisfyemailOtp.allowLogin, a new user MUST satisfyemailOtp.allowUserCreation; a policy denial still returns the enumeration-resistant success response and sends no code. - On successful Email OTP verification the one-time token is atomically marked consumed, the primary email is marked verified, and a session is issued.
- When
forceSso = trueand enterprise SSO hasenabled + allowLogin + domainDiscovery, the Hosted UI shows only the enterprise SSO email discovery form. POST /sso/hrdqueries a verified org domain only when enterprise SSO hasenabled + allowLogin + domainDiscovery, and it applies both the Hosted Auth globalallowedEmailDomains/blockedEmailDomainsand the enterprise SSOallowedEmailDomains/blockedEmailDomains; when enterprise SSO or domain discovery is denied by policy it returns{ connectionId: null }and writesauth.policy_denied.- OIDC RP JIT and SAML JIT apply both the Hosted Auth global domain policy and the enterprise SSO domain policy. When either policy denies, no user is created, no existing user is synced, and no membership is written.
/sign-upenters the unified/sign-in?intent=sign-upand MUST NOT show a fixed email + password registration form; there is no standalonePOST /auth/sign-upAPI.
Authentication: none. The apex root is the Nimbus Site product landing page, not a browser for the
repository-internal docs/ directory. English uses /docs for the documentation hub and
/<slug> for document details. The other 7 locales use lowercase locale prefixes: zh-hans, ja,
ko, fr, de, es, and pt-br; their hubs are /<locale>/docs. The exact /scim
documentation path belongs to Site, while /scim/v2/* and /scim/outbound/* remain Core protocol
routes.
| Method | Path | Current evidence | Response |
|---|---|---|---|
| GET | / |
local Site PASS | Static Nimbus product landing page with concrete capability scope, evidence boundaries, and documentation entry points |
| GET | /docs |
local Site PASS | English Nimbus documentation hub |
| GET | /getting-started |
local Site PASS | Getting started. Current source and i18n use organization semantics |
| GET | /hosted-auth |
local Site PASS | Hosted Auth. Current source and i18n use organization semantics |
| GET | /organizations |
local Site PASS | Organization, OrgUnit, project access policy, and access-request model |
| GET | /oidc-oauth |
local Site PASS | OIDC and OAuth |
| GET | /enterprise-sso |
local Site PASS | Enterprise SSO. Distinguishes inbound SAML/OIDC, downstream SaaS SAML/OIDC, and the L4 boundary |
| GET | /social-login |
local Site PASS | Social login. Distinguishes the GitHub, Google, Microsoft account, and Apple provider-ready boundary from the production-supported boundary |
| GET | /management-api |
local Site PASS | Management API. Current source and i18n use organization API key and organization resource semantics |
| GET | /scim |
local Site PASS | Exact SCIM documentation route; examples use the Core protocol path /scim/v2/organizations/{organization_id} |
| GET | /saml |
local Site PASS | SAML SSO |
| GET | /sdks |
local Site PASS | SDKs. Distinguishes current package, local evidence, and planned design |
| GET | /self-hosting |
local Site PASS | Self-hosting technical documentation |
| GET | /status |
local Site PASS | English Nimbus status shell backed by Core /v1/public/status; remains renderable when the live API is unavailable |
| GET | /<locale>/status |
local Site PASS | Localized status shell for each of the 7 non-English route segments, with complete hreflang alternates |
| GET | /(<locale>/)?status/index.md(x) |
local Site PASS | Localized Markdown downlevel and raw MDX twins; each locale section index and root corpus includes the surface |
| GET | /zh-hans |
local Site PASS | Localized Nimbus product landing page |
| GET | /zh-hans/docs |
local Site PASS | Localized Nimbus documentation hub |
| GET | /zh-hans/oidc-oauth |
local Site PASS | Localized OIDC and OAuth documentation |
| GET | /docs/getting-started |
local Site PASS | Compatibility 308 to /getting-started; query parameters are preserved |
| GET | /docs/api |
local Site PASS | Site HTTP 404; does not render public Management API technical documentation |
| GET | /docs/deployment |
local Site PASS | Site HTTP 404; does not render docs/deployment.md |
| GET | /docs/api-contracts |
local Site PASS | Site HTTP 404; does not render this file |
| GET | /docs/design |
local Site PASS | Site HTTP 404; does not render docs/design/** |
| GET | /docs/* |
local Site PASS | An unregistered topic returns the Nimbus Site HTTP 404 and does not fall through to Core or redirect to /sign-in |
The committed public allowlist is packages/types/src/public-docs.ts; Nimbus route validation lives
in apps/site/src/lib/docs-registry.ts, and the localized document AST lives in
apps/site/src/content-source/docs/documents.json. Only the hub and the 41 allowlisted technical
documents enter the Nimbus content collection. Repository-internal documents such as docs/design,
docs/api-contracts.md, and docs/deployment.md stay outside that collection and MUST return the
Site 404. /docs/api is not a public alias. Historical public aliases such as /docs/oidc,
/docs/oauth, and /docs/sso return 308 to their canonical Site paths with the query preserved.
Every published documentation page has two static twins:
/<page>/index.mdis the downleveled Markdown render for readers and retrieval pipelines. ItsSource:line points to the same page'sindex.mdx./<page>/index.mdxis the authored MDX source after localized content generation.
The Site publishes the 352-page global index at /llms.txt and the byte-stable 352-page global
corpus at /llms-full.txt. /en/llms.txt and /en/llms-full.txt each cover the 44 English pages;
each of the other seven locales publishes the same 44-page pair under its locale segment, such as
/zh-hans/llms.txt and /zh-hans/llms-full.txt. Each index links every page in its scope to the
Markdown twin. Core-owned /.well-known/llms.txt returns 308 to the canonical Site /llms.txt.
Nimbus page heads carry title, description, canonical URL, hreflang alternates, Open Graph metadata,
JSON-LD, and a Markdown alternate. The Site also owns /robots.txt, /sitemap.xml,
/sitemap-index.xml, generated OG routes, and Pagefind. A content entry marked draft: true is not
published; noindex: true excludes it from the agent index and search unless the content contract
explicitly overrides searchability.
Static asset metadata MUST produce these response types:
| Path pattern | Content-Type |
|---|---|
/*.md |
text/markdown; charset=utf-8 |
/*.mdx |
text/markdown; charset=utf-8 |
/*.txt |
text/plain; charset=utf-8 |
All Site responses, including redirects and 404 responses, carry
X-XID-Route-Owner: site. Production L4 for the new three-Worker route split remains gated on the
coordinated deployment and live owner-header smoke; local Site build evidence is not production
evidence.
Authentication: cookie session. Authorization: manager_assignments.manager_role = instance_manager and scope_type = instance.
The user.instanceManager field of GET /v1/me is used only by the same-origin console guard. It comes from a platform-layer manager_assignments query and MUST NOT be written into a business token claim.
The organizations[].role field of GET /v1/me comes from membership; for a user holding manager_assignments.manager_role = org_manager (scope_type = org), a member is returned promoted to admin, and a managed org with no membership is also backfilled into the list as admin, which matches the pass semantics of requireOrgManager.
GET /v1/me.managerAssignments is a Console discovery surface, not a token claim. It contains only
the current Tenant and current user's project control-plane scopes:
{ id, managerRole, scopeType, scopeId, scopeStatus }. A project_manager remains discoverable for
an active or deleted Project while its owning Organization is active, so the Console can restore it;
scopeStatus is active or deleted. A project_grant_manager is returned only while both the
Grant and its Project are active, with scopeStatus = active. Org managers remain represented
through organizations, and Instance Managers through user.instanceManager.
The platform view entry points live under the unified console: /console/platform,
/console/platform/organizations, /console/platform/users, /console/platform/events,
/console/platform/flags, /console/platform/billing, /console/platform/plans,
/console/platform/announcements, /console/platform/status, /console/platform/compliance,
/console/platform/dead-letters, and /console/platform/settings. /platform-admin/* is no
longer registered as a compatibility entry and MUST NOT host a standalone admin SPA. The old
/v1/platform/tenants has been removed and returns 404; public docs, SDKs, and the catalog no
longer reference the old naming.
| Method | Path | Status | Response |
|---|---|---|---|
| GET | /v1/platform/stats |
PASS | PlatformStats |
| GET | /v1/platform/tenants |
historical wrong path, production 404 | Old public contract target. The source has been removed, and local Worker HTTP with the localhost host returns 404; current main production HTTP returns 404, and public UI, docs, and SDKs do not reference it |
| PATCH | /v1/platform/tenants/:tenantId |
historical wrong path, source removed | Old public contract target. The source has been removed and it MUST NOT be treated as a public API PASS |
| GET | /v1/platform/organizations |
production L4 | The production Chrome console /console/platform/organizations has been accessed successfully with a real Instance Manager cookie. Response { data, nextCursor, total }, with items using PlatformOrganization semantics |
| PATCH | /v1/platform/organizations/:organizationId |
production L4 | Response PlatformOrganization. Local smoke:l2:platform and pnpm smoke:production:browser have verified: seed a temporary top-level organization, PATCH suspended -> active with a real cookie succeeds, then clean up the temporary org. The default organization MUST NOT be PATCHed to suspended or deleted and returns 409 conflict |
| GET | /v1/platform/users |
PASS | { data, nextCursor, total } |
| GET | /v1/platform/manager-assignments |
local PASS | Cookie-only Instance Manager list. Returns { data, nextCursor, total }; items are camelCase and contain only instance_manager / instance / null-scope assignments |
| POST | /v1/platform/manager-assignments |
local PASS | Body { user_id }; provisions an active user from any Organization as an Instance Manager. Self-provision is rejected and the assignment stores the target user's tenant as its isolation owner. The privilege mutation and platform.instance_manager.granted outbox row commit in one D1 batch before Queue delivery |
| DELETE | /v1/platform/manager-assignments/:id |
local PASS | Revokes one Instance Manager assignment; self-revocation and removal of the final Instance Manager are rejected atomically. The delete and platform.instance_manager.revoked outbox row commit in one D1 batch |
| GET | /v1/platform/audit-events |
PASS | { data, nextCursor, total } |
| GET | /v1/platform/audit/verify |
local PASS | Requires tenant_id; optional positive from_seq / to_seq. Recomputes the selected append-only chain in D1 batches of at most 1000 rows and returns { tenant_id, verified_range, chain_valid, broken_at_seq, failure_reason, record_count, computed_at }. A range after seq 1 is anchored to its stored predecessor; full verification starts at seq 1. The Console exposes the same diagnostic. An asynchronous Queue/KV job is not implemented |
| GET | /v1/platform/dead-letters |
PASS | Cursor-paginated redacted Queue dead-letter metadata. Ciphertext and plaintext payloads are never returned |
| GET | /v1/platform/dead-letters/:id |
PASS | One redacted Queue dead-letter record. Requires a verified instance_manager cookie session |
| POST | /v1/platform/dead-letters/:id/replay |
PASS | Claims the KEK-encrypted original message only for its recorded source Queue, rejects a concurrent live lease, reclaims a lease older than five minutes, and emits an audit event after completion. Completed records are idempotent; crash recovery is at-least-once because Queue acceptance can precede the D1 completion write |
| GET | /v1/platform/billing |
PASS | { data, nextCursor, total } |
| GET | /v1/platform/feature-flags |
PASS | FeatureFlag[] |
| PATCH | /v1/platform/feature-flags/:key |
PASS | FeatureFlag |
| GET | /v1/platform/settings |
PASS | PlatformSettings: instance default locale, MFA policy, password/session policy; Workers Secrets are never echoed back |
| PATCH | /v1/platform/settings |
PASS | PlatformSettings; can update mfaPolicy, passwordPolicy, sessionPolicy, defaultLocale; gated by an instance_manager cookie session |
| GET | /v1/platform/plans/:tenantId |
local PASS | Returns the top-level Organization's accounting plan, status, source, support label, trial/effective timestamps, seat limit, and persisted quota rows. This is local D1/route evidence only and does not prove an external billing-provider or production L4 path |
| PATCH | /v1/platform/plans/:tenantId |
local PASS | Updates plan, status, trial end, seat limit, and quota rows in one D1 batch with a durable platform audit intent. Seat limits use block_creation; usage counters such as API calls, email, and MAU remain observe-only. No external billing-provider or production L4 behavior is claimed |
| GET | /v1/platform/billing/stripe-config |
local PASS, provider L4 UNKNOWN |
Optional adapter readiness; accepts optional tenantId query and returns enabled Checkout plans, Portal readiness, and MAU metering readiness without exposing any Stripe credential |
| POST | /v1/platform/billing/checkout |
local PASS, provider L4 UNKNOWN |
Body { tenantId, plan, idempotencyKey }; Instance Manager only. Creates a Stripe-hosted Checkout session with tenant/plan metadata and a caller-stable idempotency key. Requires both Stripe secrets plus the matching plan price id |
| POST | /v1/platform/billing/portal |
local PASS, provider L4 UNKNOWN |
Body { tenantId }; Instance Manager only. Creates a Stripe-hosted Billing Portal session for the reconciled external customer. Requires both Stripe secrets and an existing customer binding |
| POST | /v1/platform/impersonation/start |
PASS | Body { userId, organizationId }; an Instance Manager receives an opaque two-minute form-POST handoff descriptor for the exact target Organization host. Target identity never appears in the grant fields or URL |
| POST | /auth/impersonation/consume |
PASS | Body { grantId, secret }; exact target-host TenantContext plus ImpersonationGrantDO consume-once validation creates a 15-minute session restricted to GET / HEAD / OPTIONS under /v1/* and explicit impersonation end; protocol, auth, SSO, session-token exchange, and every mutation path return 403 |
| POST | /auth/impersonation/handoff |
PASS | application/x-www-form-urlencoded twin of consume; succeeds with 303 /console, never places the opaque grant in a query string |
| POST | /auth/impersonation/end |
PASS | Requires the current impersonation session, durably records the end through platform_audit_outbox, revokes the session, and returns { ok: true, redirectUrl }; redirectUrl is derived from the target TenantContext instance issuer and returns the operator to /console/platform/users on the manager host |
| GET | /v1/platform/announcements |
local PASS | Cursor-paginated global, tenant, and accounting-plan announcement ledger |
| POST | /v1/platform/announcements |
local PASS | Creates a draft or published time-bounded announcement and a redacted durable platform audit intent |
| PATCH | /v1/platform/announcements/:id |
local PASS | Updates targeting, content, severity, publication state, or time window; the mutation and audit intent commit in one D1 batch |
| DELETE | /v1/platform/announcements/:id |
local PASS | Deletes the announcement and durably records the operator action |
| GET | /v1/platform/status-incidents |
local PASS | Cursor-paginated incident ledger with timestamped updates |
| GET | /v1/platform/status-incidents/:id |
local PASS | One incident and its append-only update timeline |
| POST | /v1/platform/status-incidents |
local PASS | Opens an incident; accepted impact values are none, minor, major, and critical |
| PATCH | /v1/platform/status-incidents/:id |
local PASS | Changes incident metadata or state; resolved_at follows the resolved state |
| POST | /v1/platform/status-incidents/:id/updates |
local PASS | Appends a timestamped state update and atomically advances the incident |
| DELETE | /v1/platform/status-incidents/:id |
local PASS | Deletes an incident plus its updates in the same D1 batch and records a durable audit intent |
| GET | /v1/platform/compliance-documents |
local PASS | Cursor-paginated versioned global and tenant-specific evidence metadata |
| POST | /v1/platform/compliance-documents |
local PASS | Registers metadata for an immutable compliance/ R2 object and required lowercase sha256: checksum |
| PATCH | /v1/platform/compliance-documents/:id |
local PASS | Publishes, retires, or corrects evidence metadata before tenant acceptance; accepted evidence is immutable |
| GET | /v1/platform/compliance-documents/:id/artifact |
local PASS | Authenticated private attachment proxy; rejects unsafe keys, objects over 10 MiB, and checksum mismatches |
| DELETE | /v1/platform/compliance-documents/:id |
local PASS | Deletes only when the document itself is unaccepted and no Organization acceptance copy references it. The delete predicate and tenant acceptance write both verify this relation inside their D1 batch, so a concurrent acceptance cannot leave an orphaned evidence record |
Authentication: none. The response exposes only published incident state and timestamped public updates; it contains no tenant, operator, or audit metadata.
| Method | Path | Status | Response |
|---|---|---|---|
| GET | /v1/public/status |
local PASS | Overall state derived from unresolved incident impact plus the current public incident timeline |
POST /v1/billing/stripe/webhook is also public because Stripe cannot hold an XID cookie. It is not
a public management mutation: Core first verifies the raw request body with
Stripe-Signature, enforces the timestamp tolerance, validates the event shape, and only then
reconciles the top-level Organization plan, quotas, seat mirror, durable audit intent, and event
receipt in one D1 batch. Duplicate ids are idempotent and older or lower-priority same-timestamp
events cannot revert newer state. Local SQLite/route evidence is PASS; real Stripe delivery and
provider L4 remain UNKNOWN.
These paths use the request TenantContext. Active announcements require an active cookie session;
compliance list, download, and DPA acceptance additionally require Organization Manager access to the
tenant root.
| Method | Path | Status | Response |
|---|---|---|---|
| GET | /v1/announcements/active |
local PASS | Active published global, current-tenant, and current accounting-plan announcements; private cache for 30 seconds |
| GET | /v1/compliance/documents |
local PASS | Available global plus tenant-specific evidence, with a tenant version taking precedence for the same document type and version |
| GET | /v1/compliance/documents/:id/artifact |
local PASS | Private checksum-verified evidence attachment |
| POST | /v1/compliance/documents/:id/accept |
local PASS | DPA only; creates an immutable tenant acceptance record with actor, timestamp, source version, and checksum; concurrent retries converge idempotently |
Authentication: cookie session. Authorization: the current user MUST hold an admin or owner membership in the target org, or a manager_assignments row with org_manager / instance_manager. The same path presented with Bearer sk_* is still authenticated as a Management API key.
org_manager is narrowed to the target org by the internal isolation query layer; instance_manager is resolved through the platform-layer ManagerAssignment query and does not require the current session active org to equal the target org. The current platform list enters the same policy configuration page through /console/org/auth-policy?orgId=<org_id> and MUST NOT add a standalone admin app. Future public copy MUST say Organizations and MUST NOT say Platform tenants.
| Method | Path | Status | Response |
|---|---|---|---|
| GET | /v1/organizations/:orgId/stats |
PASS | OrgStats, containing dau, mau, loginSuccessRate, mfaAdoptionRate, activeMemberCount, pendingInvitationCount |
| GET | /v1/organizations/:orgId/members |
PASS | { data, nextCursor, total } |
| DELETE | /v1/organizations/:orgId/members/:memberId |
PASS | 204; writes memberships.status = inactive |
| GET | /v1/organizations/:orgId/invitations |
PASS | { data, nextCursor, total } |
| POST | /v1/organizations/:orgId/invitations |
PASS | OrgInvitation |
| DELETE | /v1/organizations/:orgId/invitations/:invitationId |
PASS | 204; writes invitations.status = revoked |
| GET | /v1/organizations/:orgId/roles |
PASS | OrgRole[] |
| GET | /v1/organizations/:orgId/sso-connections |
PASS | SsoConnection[] |
| POST | /v1/organizations/:orgId/sso-connections |
PASS | SsoConnection; supports first-version SAML/OIDC configuration and returns the editable fields: IdP metadata, certificate, OIDC discovery, JIT, attribute mapping, role mapping, signature requirements |
| PATCH | /v1/organizations/:orgId/sso-connections/:connectionId |
PASS | SsoConnection; can update SAML/OIDC connection configuration, JIT, attribute mapping, role mapping, and signature requirements; the target connection MUST belong to the current org and MUST be active |
| DELETE | /v1/organizations/:orgId/sso-connections/:connectionId |
PASS | 204; writes sso_connections.status = deleted, and normal lists filter out deleted |
| GET | /v1/organizations/:orgId/directories |
PASS | ScimDirectory[] |
| POST | /v1/organizations/:orgId/directories |
PASS | CreatedScimDirectory; scimToken is returned in this response only |
| POST | /v1/organizations/:orgId/directories/:directoryId/rotate-token |
PASS | RotateScimTokenResult; scimToken is returned in this response only |
| GET | /v1/organizations/:orgId/domains |
PASS | OrgDomain[] |
| POST | /v1/organizations/:orgId/domains |
PASS | OrgDomain |
| GET | /v1/organizations/:orgId/custom-hostnames |
local PASS, provider L4 UNKNOWN |
Cursor-paginated { data, next_cursor, has_more }; org-scoped custom_hostnames:read; returns lifecycle state, ownership expiry, ownership TXT, DCV CNAMEs, traffic CNAME, verification codes and requires_passkey_reregistration; deleted tombstones are hidden |
| GET | /v1/organizations/:orgId/custom-hostnames/:customHostnameId |
local PASS, provider L4 UNKNOWN |
One non-deleted custom hostname from the target org; a cross-tenant id is the same opaque 404 as a missing id |
| POST | /v1/organizations/:orgId/custom-hostnames |
local PASS, provider L4 UNKNOWN |
Body { hostname }; rejects schemes, paths, ports, IP/private/special-use hosts, wildcard hosts and the instance primary domain or its subdomains. Globally reserves the normalized hostname before Cloudflare create. Returns ownership/DCV/traffic DNS instructions and the passkey warning flag. A cross-tenant hostname collision is an opaque 409 already_exists |
| POST | /v1/organizations/:orgId/custom-hostnames/:customHostnameId/refresh |
local PASS, provider L4 UNKNOWN |
Re-reads the exact Cloudflare object and updates hostname/SSL/DCV state. A mismatched provider id or hostname fails closed with 503. Local active requires both provider hostname and SSL status to be active |
| DELETE | /v1/organizations/:orgId/custom-hostnames/:customHostnameId |
local PASS, provider L4 UNKNOWN |
Calls Cloudflare delete before local deletion. Remote failure records deletion_failed and retains the binding. Success preserves a global hostname tombstone and returns the traffic CNAME the operator must remove |
| GET | /v1/organizations/:orgId/auth-policy |
PASS | OrgAuthPolicy; returns only the Hosted Auth policy and the read-only deliveryChannelReadiness.whatsappOtp/smsOtp. deliveryChannelReadiness is computed live from Workers env and delivery channel secret completeness and MUST NOT be read from the request body or the D1 policy; Social provider configuration and the old providerReadiness field are not returned |
| PATCH | /v1/organizations/:orgId/auth-policy |
PASS | Saves the Hosted Auth policy only. Even when the request body contains socialProviders, providerReadiness, deliveryChannelReadiness, or credentialsReady, it MUST NOT write or overwrite Social provider configuration or delivery channel readiness |
| GET | /v1/organizations/:orgId/social-providers |
PASS | OrgSocialProviders. Returns provider connection configuration but MUST NOT return clientSecretRef; it returns only hasClientSecret and credentialsReady. hasClientSecret means a secret ref is stored in the policy, credentialsReady means enabled + clientId + authorizationEndpoint + tokenEndpoint + clientSecretRef + Workers Secret are all complete; an OIDC provider additionally requires a complete issuer and JWKS URI, and GitHub uses a dedicated profile flow |
| PATCH | /v1/organizations/:orgId/social-providers |
PASS | Saves the Social provider set. When the request contains socialProviders, that object is the provider set after saving and any missing provider is deleted; when a provider field is present, clearing optional strings, redirect URIs, scopes, allowed domains, and blocked domains is allowed; when a field is absent the old value is kept. The provider secret ref is never echoed back, and a normal save that omits the secret ref MUST NOT erase the existing secret ref. credentialsReady is a read-only field: even when supplied in the request body it is ignored and recomputed from the current Workers env |
| GET | /v1/organizations/:orgId/branding |
PASS | OrgBranding, KV key brand:{tenant_id}:{org_id} |
| PATCH | /v1/organizations/:orgId/branding |
PASS | OrgBranding, KV key brand:{tenant_id}:{org_id} |
| GET | /v1/organizations/:orgId/outbound-saml-apps |
PASS | OutboundSamlApp[]; returns downstream SaaS SAML app configuration, ACS URL, NameID policy, assignmentGate (mode / allowed_user_ids / allowed_roles), and provider preset metadata, and never echoes back the secret ref |
| POST | /v1/organizations/:orgId/outbound-saml-apps |
PASS | OutboundSamlApp; supports Slack, GitHub Enterprise, Microsoft custom app, Atlassian, Salesforce, and Zoom preset fields; optional assignment_gate; local L3 already covers metadata and SSO POST with a fake SaaS SP |
| PATCH | /v1/organizations/:orgId/outbound-saml-apps/:appId |
PASS | OutboundSamlApp; can update ACS, SLO, attribute mapping, assignment_gate, and preset metadata; restricted mode gates outbound SSO launch by allowed_roles and allowed_user_ids |
| DELETE | /v1/organizations/:orgId/outbound-saml-apps/:appId |
PASS | 204; hard-deletes the saml_service_providers row (the table has no status column; after deletion the metadata/SSO routes return 404) |
| GET | /v1/organizations/:orgId/scim-targets |
PASS | ScimTarget[]; downstream SaaS SCIM push configuration; returns baseUrl, assignmentGate, requiredTokenSecretName, hasTokenSecret, and syncPath. The server derives the only accepted Workers Secret name as SCIM_TARGET_TOKEN_<normalized target id>; it never accepts or echoes a tenant-selected token_secret_ref |
| POST | /v1/organizations/:orgId/scim-targets |
PASS | ScimTarget; the body MUST contain provider and a public HTTPS base_url, and MAY contain assignment_gate. The server creates the target id and returns requiredTokenSecretName; callers configure that exact Workers Secret out of band. Supplying token_secret_ref is rejected. Requires the connections:write scope or an org admin cookie |
| PATCH | /v1/organizations/:orgId/scim-targets/:targetId |
PASS | ScimTarget; can update provider, a public HTTPS base URL, and assignment_gate. Supplying token_secret_ref is rejected; a tenant cannot select an arbitrary Worker binding |
| DELETE | /v1/organizations/:orgId/scim-targets/:targetId |
PASS | 204; writes scim_targets.status = deleted |
| POST | /v1/organizations/:orgId/scim-targets/:targetId/sync |
PASS | 202 { runId, targetId, status: "queued" }; authorizes and enqueues the outbound run without calling the SaaS in the request path. The consumer applies the Organization Membership plus assignment-gate intersection, persists stable downstream User/Group mappings, performs idempotent upsert/deprovision, retries 408/429/5xx, and emits correlated audit events |
| GET | /v1/organizations/:orgId/delivery-channels |
PASS | OrgDeliveryChannels; WhatsApp/SMS OTP and MFA delivery channel configuration; credentialsReady is computed live from Workers env, and the secret ref is never echoed back |
| PATCH | /v1/organizations/:orgId/delivery-channels |
PASS | OrgDeliveryChannels; saves the provider choice and secret ref; an omitted secret ref MUST NOT erase the existing value |
| GET | /v1/organizations/:orgId/audit-events |
PASS | { data, nextCursor, total }; org-scoped read-only audit stream; org_manager sees only this org, instance_manager can query across orgs; cursor pagination |
Authentication: cookie session for self-service org creation. Invitation claim initiation and verification are public, capability-bound endpoints; all invitation holders use them, including callers that already have a session. Covers organization invitations, self-service org creation, Device Flow activation, and CIBA user approval. The public product semantic is Organization; tenant_id is internal isolation only.
| Method | Path | Status | Response |
|---|---|---|---|
| GET | /auth/invitation/preview |
PASS | Query MUST contain the raw token. Its locator selects a same-Instance candidate Tenant, but the complete token hash must match inside that Tenant before metadata is returned. Signed User/Session continuations are not accepted. Invalid and expired capabilities return a 200 preview with status = invalid or expired. The capability authorizes an attempt; preview never proves Email ownership or selects a User |
| POST | /auth/invitation/claim |
PASS | Body { token, turnstileToken? }. Returns the opaque { ok: true } without revealing invitation validity or account existence. For a valid pending capability, resolves through the target Tenant's scoped database and sends only to the invitation's exact normalized Email. The signed claim JWT has purpose = invitation_email_claim, tenant_id, sub = invitationId, jti, and email_hash, expires after 15 minutes, and is single use. This endpoint performs no User lookup/reuse or write and persists no password, phone, social identity, passkey, MFA factor, session, or Membership |
| POST | /auth/invitation/claim/verify |
PASS | Body { token, recoveryKey }, where recoveryKey is a browser-generated random string of 32-256 characters. Verifies the instance signature, purpose, Tenant, invitation row, exact Email hash, expiry, and first-use jti. The proof winner atomically marks the stored SHA-256(jti) consumed, freezes a random server-side consumption id, binds SHA-256(recoveryKey), and commits pending -> claim_verified. It reuses only an exact active User plus verified-primary Email row carrying durable invitation_email_claim_v1 provenance; every other collision detaches only that Email row and creates a credential-free User without transferring or scrubbing the old account. The original signed claim plus the same recovery key then issues or recovers one session, applies the post-auth MFA gate, creates/reactivates the Membership, and commits claim_verified -> accepted. Accepted retries return the same { redirectUrl } and may repair that browser session, but never create another Membership or emit another acceptance webhook |
| POST | /auth/invitation/accept |
PASS | Always rejects with the opaque invitation_invalid response. Raw or signed-continuation acceptance is disabled; every invitation holder uses /auth/invitation/claim and /auth/invitation/claim/verify |
| POST | /v1/organizations/self |
PASS | A signed-in user self-service creates an organization; the body MUST contain slug and name; creates the owner membership, switches the active org, and returns { organization, redirectUrl } |
| GET | /auth/device-activation |
PASS | Device Flow user-side view; query user_code; returns the client name, scope summary, and expiry; returns 401 when not signed in |
| POST | /auth/device-activation |
PASS | Device Flow approve or deny; body { userCode, approved }; on approval binds userId to the device code; returns 401 when not signed in |
| GET | /auth/ciba-activation |
PASS | CIBA user-side view; query auth_req_id; returns authReqId, clientId, scope, loginHint, expiresAt, and firstParty; returns 401 when not signed in |
| POST | /auth/ciba-activation |
PASS | CIBA approve or deny; body { authReqId, approved }; returns 401 when not signed in |
Both invitation claim endpoints re-resolve the active target Organization inside the token's
trusted Instance. Claim start checks its Email and Magic Link user-creation policy but folds a
policy denial into the same opaque { ok: true }. Claim verification rechecks the target policy
before any proof-stage write and uses that Organization's MFA enforcement when selecting
active, pending_mfa_setup, or pending_mfa; it never falls back to the Instance-root policy.
Recovery does not extend the Email proof: every retry still verifies the original 15-minute signed
claim as well as the browser-owned recovery key.
Hosted UI SPA routes: /accept-invitation, /create-organization, /select-organization, /activate.
The initial invitation entry uses the raw token query parameter, but that capability is never
authentication or proof of its Email. An unauthenticated visitor must use the dedicated claim
endpoints; generic password, phone, social, passkey, MFA, magic-link, and OTP flows MUST NOT create or
reuse a User from the invitation before proof. The claim message is sent only to the invitation's
exact normalized Email, and the raw invitation token is never persisted in the claim row or signed
claim JWT. A signed-in user follows the same claim path; a session or verified flag alone never
selects an existing User for invitation acceptance. Reuse requires the exact primary Email/User
tuple previously created by an invitation_email_claim_v1 ceremony. Every other exact collision
detaches only that Email association plus its stale pointers/artifacts and creates a credential-free
invited User, without transferring or scrubbing the old account. The browser-owned recovery key
binds proof, session recovery, and acceptance to the same claimant without becoming a server-stored
bearer secret. Raw or signed-continuation /auth/invitation/accept is not an acceptance path: the
compatibility route remains registered only to return the opaque invitation_invalid rejection. A
successful product sign-up without an invitation defaults to /create-organization.
Migration 0006_expire_legacy_invitation_tokens.sql revokes pre-xid_inv_v1 pending capabilities
because their hash-only rows cannot restore a Tenant from the Instance apex without a global lookup.
The retained revoked rows are the resend audit list. New create and bulk-create paths always persist
the internal token_version = locator_v1; neither tokenHash nor tokenVersion is returned by the
Management API. Migration 0011_invitation_email_claim.sql adds proof/recovery markers, normalizes
historical pending Emails, deterministically revokes older duplicate (tenant_id, org_id, email)
rows, then creates a partial unique index covering pending and claim_verified. Create and bulk
create reject an existing pending claim with HTTP 409. They reject an already-active member only
when the exact primary Email/User tuple carries accepted invitation_email_claim_v1 provenance;
an ordinary verified or unverified Email collision cannot block a rightful owner from starting the
proof flow. Management list/detail surfaces map internal claim_verified to pending, and revoke or
delete accepts both states, revoking any reserved claim session before a new invitation can be
created. API responses expose neither claim internals nor displaced identity references.
Authentication: cookie session. Authorization: only resources belonging to the current session userId may be operated on, with tenant_id injected by createTenantDb.
| Method | Path | Status | Response |
|---|---|---|---|
| GET | /v1/me |
PASS | Browser account summary. Anonymous requests return { user: null, activeOrg: null, organizations: [], managerAssignments: [], session: null, activeSessionId: null, sessions: [] }. Signed-in responses include the active session, only other sessions backed by valid refresh cookies held by this browser, and active-valid Project/Grant ManagerAssignment discovery metadata described above |
| GET | /v1/me/profile |
PASS | UserProfile |
| PATCH | /v1/me/profile |
PASS | UserProfile |
| GET | /v1/me/passkeys |
PASS | PasskeyCredential[] |
| PATCH | /v1/me/passkeys/:id |
PASS | PasskeyCredential |
| DELETE | /v1/me/passkeys/:id |
PASS | 204 |
| GET | /v1/me/mfa-factors |
in progress, negative production L4 covered | MfaFactor[]; SMS is returned only when there is a verified phone and the SMS provider is ready. Local L2/L3 verified that the MFA UI does not always show SMS; in production with no SMS provider configured, a real cookie verified that SMS is not returned and that /mfa?method=sms shows no SMS challenge and no secondary menu. The positive provider-ready L4 still lacks a real SMS provider secret and a verified phone number |
| DELETE | /v1/me/mfa-factors/:id |
PASS | 204 |
| GET | /v1/me/social-connections |
PASS | SocialConnection[] |
| DELETE | /v1/me/social-connections/:id |
PASS | 204 |
| GET | /v1/me/trusted-devices |
PASS | TrustedDevice[] |
| DELETE | /v1/me/trusted-devices/:id |
PASS | 204 |
| GET | /v1/me/sessions |
PASS | ActiveSession[] |
| DELETE | /v1/me/sessions/:id |
PASS | 204 |
| POST | /v1/me/sessions/revoke-all |
PASS | { revoked: true } |
| GET | /v1/me/privacy/requests |
local PASS | Up to the current user's 100 newest privacy requests, tenant and user scoped. Export responses expose an authenticated relative downloadUrl only while the private R2 object is available and unexpired |
| POST | /v1/me/privacy/requests |
local PASS | Accepts type: export, or type: delete together with the exact destructive confirmation confirmation: DELETE. The Account UI exposes deletion only through a second confirmation dialog. Export is queued immediately; deletion is scheduled after a cancelable 30-day grace period. Scheduling returns an opaque 409 conflict when erasure would remove an Organization's sole active owner or the last active instance_manager in the same Instance scope; the Queue consumer rechecks after the grace period and its D1 batch atomically rolls back if roles changed. A concurrent pending or processing request of the same type is returned instead of duplicated |
| GET | /v1/me/privacy/requests/:id |
local PASS | Returns only a request owned by the current tenant and session user |
| POST | /v1/me/privacy/requests/:id/cancel |
local PASS | Cancels a pending request. An already canceled request is idempotent; processing or terminal requests return 409 |
| GET | /v1/me/privacy/requests/:id/download |
local PASS | Streams only the current user's completed, unexpired export with private, no-store, attachment, sandbox, and nosniff headers. The URL is not a public or signed R2 URL |
| POST | /v1/sessions/token |
PASS | Exchanges a normal active cookie session for a short-lived first-party JWT and returns { token }; an impersonation cookie is rejected with HTTP 403 at the global impersonation boundary |
| POST | /v1/sessions/active |
PASS | Accepts { sessionId }; the matching browser-held HttpOnly refresh cookie is the credential. On success updates the HttpOnly active-session pointer and returns { activeSessionId }; an id without its valid cookie returns an opaque 401 |
| POST | /v1/sessions/active-organization |
production L4 | The request body MUST explicitly include organizationId with a value of string or null. For a non-empty string it validates the current user's active membership and that the organization is active and not soft deleted; null only clears the current session active org. An invalid body returns 422 with meta.paramName=organizationId. The Worker currently returns a lightweight { session, activeOrganizationId }; the SPA auth context and the core SDK MUST re-read /v1/me after success. Focused L1, local Worker HTTP L2, local browser L3, and production browser L4 all exist |
Authentication: Bearer sk_live_* or Bearer sk_test_*. Tenant isolation: TenantContext + createTenantDb. API key scope rules:
scopes = []: no permission. Both write and read operations MUST match an explicit scope.resource:read: allows GET / export style read operations on the matching resource.resource:write: allows POST / PATCH / PUT / DELETE / revoke / rotate style write operations on the matching resource.resource:*: allows read and write on the matching resource.*: allows every Management API resource.- Successful authentication with insufficient scope returns
403andcode = insufficient_permission.
/v1/projects, /v1/roles, /v1/permissions, /v1/role-permissions,
/v1/manager-assignments, /v1/project-grants, and /v1/user-grants additionally accept a
same-origin cookie session. That path is authorized against an exact Organization, Project, or
ProjectGrant ManagerAssignment and never promotes a Project manager role to Organization Admin.
| Resource | Status | Confirmed |
|---|---|---|
/v1/users |
PASS | CRUD, ban, unban, bulk metadata, export, and restore exist; delete is a soft delete, and normal lists and detail filter out deleted; restore clears deleted_at and returns the row to active, and returns 409 already_exists when an active non-deleted user already holds the same username or external_id; username and external_id are unique among active non-deleted users, and a soft deleted user releases the identifier; scopes users:read / users:write are tested |
/v1/organizations |
PASS | CRUD, logo, domains, and restore exist. POST creates one-level child organizations only: parent_org_id is required and must identify the current active top-level Organization; cross-Tenant parents, child-of-child creation, and implicit reparenting are rejected. Child delete/restore is soft; top-level status changes stay on the separate Instance Manager path. POST rejects seat_limit. PATCH accepts seat_limit only for the root tenant and atomically updates both its compatibility mirror and the authoritative organization_quotas(seats) hard quota. Service validation and migration-owned triggers enforce the hierarchy. Scopes organizations:read / organizations:write are tested |
/v1/api-keys |
PASS | Bearer sk_* goes through scopes api_keys:read / api_keys:write; a cookie session is gated by instance_manager; create returns the plaintext key once only; revoke semantics exist, normal lists and detail filter out revoked, and a revoked key can no longer authenticate; API keys provide no restore |
/v1/projects |
local PASS | Full CRUD with scopes projects:read / projects:write. Delete is reversible (status = deleted, deleted_at set); restore requires the owning Organization to remain active. Every shared client lookup, including token refresh and client credentials, rejects applications linked to a deleted Project. List defaults to status=active and accepts deleted or all; cookie sessions must still supply org_id or exact project_id. Organization owner/admin creates; exact project_manager can read/update/delete/restore only its Project |
/v1/roles |
PASS | Delete is a soft delete and restore clears deleted_at. List defaults to status=active, accepts deleted or all, and returns status plus deleted_at; detail remains active-only. API keys use roles:read / roles:write. Cookie reads require project_id; a Grant-scoped reader also supplies grant_id. Only the Project owner/admin or exact project_manager can mutate definitions |
/v1/permissions |
PASS | Delete is a soft delete and restore clears deleted_at. List defaults to status=active, accepts deleted or all, and returns status plus deleted_at; detail remains active-only. API keys use permissions:read / permissions:write. Cookie reads require project_id; a Grant-scoped reader also supplies grant_id. Only the Project owner/admin or exact project_manager can mutate definitions |
/v1/role-permissions |
local PASS | List/create/update/delete with scopes role_permissions:read / role_permissions:write. Role and Permission must both be active, tenant-scoped, and belong to the same active Project. condition_expression accepts only the documented ABAC v1 leaf or non-empty and grammar; malformed JSON structure, operator, variable path, or in operand is rejected with 422 before persistence |
/v1/manager-assignments |
local PASS | Tenant-scoped list/provision/revoke with scopes manager_assignments:read / manager_assignments:write. Only org_manager/org, project_manager/project, and project_grant_manager/grant pairs are accepted. Target user and scope must be active in the current tenant. Cookie actors cannot modify their own assignment; Project and Grant manager provisioning requires the owning Organization manager boundary |
/v1/applications |
PASS | Management API delete sets status = deleted, and restore returns the row to active but does not return the client secret; secret rotate and detail accept active only; scopes applications:read / applications:write |
/v1/connections |
PASS | delete sets status = deleted, and restore returns the row to active; the SSO runtime path accepts active only; scopes connections:read / connections:write |
/v1/directories |
PASS | delete sets status = deleted and sync_status = disabled; restore returns the row to active, resets sync_status = idle, regenerates the SCIM token, and returns scim_token in this response only; the SCIM token accepts an active directory only; scopes directories:read / directories:write |
/v1/webhooks |
PASS | delete sets status = deleted, and restore returns the row to active but does not return the signing secret; normal lists and detail filter out deleted; scopes webhooks:read / webhooks:write |
/v1/organizations/:orgId/memberships |
PASS | delete sets status = inactive, and normal lists and detail filter out inactive; restore returns the row to active and re-validates that the target user is active and deleted_at IS NULL; scopes memberships:read / memberships:write. API keys cannot create, promote, restore, or reactivate an owner; demote, deactivate, and delete use one conditional D1 mutation that preserves another active owner backed by an active user |
/v1/organizations/:orgId/invitations |
PASS | delete and revoke set status = revoked; normal lists default to pending and detail returns pending only; invitation tokens provide no restore; scopes invitations:read / invitations:write |
/v1/project-grants |
PASS | List/get/create/revoke/delete exist; delete/revoke set status = revoked and revoked_at, and cascade to user_grants.revoked_at. API keys use project_grants:read / project_grants:write. Cookie list is narrowed by granted_project_id or granted_to_org_id; exact project_grant_manager and recipient Org admin can read a grant, but only the Project owner/admin or exact project_manager can create or revoke it |
/v1/user-grants |
local PASS | List/get/create/reactivate/revoke/delete exist; normal list/detail return unrevoked rows only. API keys use user_grants:read / user_grants:write. Cookie writes require an exact Project owner/admin, project_manager, active project_grant_manager, or recipient Org owner/admin boundary. Grant-scoped writes accept only an active member of the recipient Org and a Role in the granted Project |
/v1/sessions |
PASS | list/get/revoke/revoke_all exist; revoke and revoke_all set status = revoked; normal lists and detail return active only; revoke_all rejects a deleted user; scopes sessions:read / sessions:write |
/v1/organizations/:orgId/custom-hostnames |
local PASS, provider L4 UNKNOWN |
Org-scoped list/get/create/refresh/delete; scopes custom_hostnames:read / custom_hostnames:write; global hostname reservation, remote-first deletion, status polling, active-only TenantContext reverse lookup and Console passkey warning are covered locally. No production Cloudflare for SaaS, customer DNS, certificate or traffic evidence exists yet |
All tenant routes below use { data, next_cursor, has_more } for lists. Project responses are
{ id, org_id, name, description, status, deleted_at, created_at, updated_at }.
RolePermission responses are
{ id, role_id, permission_id, condition_expression, created_at }.
ManagerAssignment responses are
{ id, user_id, manager_role, scope_type, scope_id, created_at, updated_at }.
Role and Permission responses include status and deleted_at; their list routes accept
status=active, status=deleted, or status=all and default to active.
Tenant-scoped Project, Role, Permission, RolePermission, and ManagerAssignment mutations enqueue
redacted management.* records through AUDIT_QUEUE after the scoped D1 mutation. This path is
asynchronous and is not yet atomic with the relational write. Instance Manager grant/revoke uses
the persisted platform audit outbox and is atomic with its privilege mutation.
| Method | Path | Request / behavior |
|---|---|---|
| GET | /v1/projects |
Query org_id?, project_id?, grant_id?, status?, limit?, cursor?; status accepts active, deleted, or all and defaults to active. Cookie sessions require org_id or project_id; an exact ProjectGrant reader supplies grant_id |
| POST | /v1/projects |
{ org_id, name, description? } |
| GET | /v1/projects/:id |
Active Project only |
| PATCH | /v1/projects/:id |
{ name?, description? }; description: null clears it |
| DELETE | /v1/projects/:id |
204; soft delete |
| POST | /v1/projects/:id/restore |
Restores a deleted Project under its still-active owning Organization |
| GET | /v1/roles |
Query project_id?, grant_id?, status?, limit?, cursor?; status accepts active, deleted, or all, defaults to active, and cookie sessions require project_id |
| GET | /v1/permissions |
Query project_id?, grant_id?, status?, limit?, cursor?; status accepts active, deleted, or all, defaults to active, and cookie sessions require project_id |
| GET | /v1/role-permissions |
Query role_id?, grant_id?, limit?, cursor?; cookie sessions require role_id |
| POST | /v1/role-permissions |
{ role_id, permission_id, condition_expression? } |
| GET | /v1/role-permissions/:id |
API key or exact Project/Grant read boundary |
| PATCH | /v1/role-permissions/:id |
{ condition_expression }; use null for an unconditional mapping |
| DELETE | /v1/role-permissions/:id |
204; physically removes only the mapping |
| GET | /v1/manager-assignments |
Query scope_type?, scope_id?, manager_role?, user_id?, limit?, cursor?; cookie sessions require the exact scope_type and scope_id |
| POST | /v1/manager-assignments |
{ user_id, manager_role, scope_type, scope_id }; fixed role/scope pairs only |
| DELETE | /v1/manager-assignments/:id |
204; tenant assignment only. instance_manager rows are deliberately invisible here |
| Area | Status | File entry |
|---|---|---|
| OIDC discovery | PASS | apps/server/worker/oidc/discovery.ts |
| OIDC authorize | PASS | apps/server/worker/oidc/authorize.ts |
| OIDC token | PASS | apps/server/worker/oidc/token.ts |
| OIDC userinfo | PASS | apps/server/worker/oidc/userinfo.ts |
| OAuth PAR | PASS | apps/server/worker/oidc/par.ts |
| OAuth device flow | PASS | apps/server/worker/oauth/device.ts |
| OAuth revoke | PASS | apps/server/worker/oauth/revoke.ts |
| OAuth introspection | PASS | apps/server/worker/oauth/introspect.ts |
| OAuth protected resource metadata | PASS | apps/server/worker/oidc/protected-resource.ts |
| OAuth token exchange | PASS | apps/server/worker/oidc/token-exchange.ts |
| OIDC request object | PASS | apps/server/worker/oidc/request-object.ts |
| OIDC RAR authorization details | PASS | apps/server/worker/oidc/authorization-details.ts |
| SCIM users and groups | provider-ready, real IdP provisioning L4 missing | XID acts as an inbound SCIM Service Provider. apps/server/worker/scim/**; the public path is /scim/v2/organizations/{organization_id}. Local L3 already covers Users/Groups CRUD, filter, projection, and the enterprise extension; local/production paths and a 401 without Bearer are not real IdP provisioning L4; real Microsoft Entra/Okta/Auth0/Clerk/Zitadel provisioning into XID is still missing |
| SCIM downstream SaaS target clients | implemented, real SaaS L4 missing | XID acts as an outbound SCIM client pushing users and groups to the Slack/GitHub Enterprise Cloud/Atlassian/Salesforce/Zoom SCIM API. Both sync endpoints enqueue xid-scim-sync; the serialized consumer uses scim_target_resources plus deterministic externalId discovery for idempotent User/Group upsert, maps Group members to downstream User ids, deprovisions only after a complete upsert phase, honors Retry-After, and records the run through AUDIT_QUEUE. The Organization Membership intersection remains the mandatory upper bound before the assignment gate. Real Slack/GitHub/Atlassian/Salesforce/Zoom admin L4 is still missing; inbound SCIM Service Provider evidence MUST NOT be reused as SCIM push-to-SaaS L4, and public docs MUST NOT promise production-supported |
| SAML inbound SSO | provider-ready, L4 missing | XID acts as the SAML SP for an upstream enterprise IdP. /sso/saml/:connection/acs, /metadata, /login; the local fake IdP L3 (smoke:l3:inbound-saml) already covers metadata refresh, ACS POST, and JIT. Real IdP metadata/config and production L4 are still missing |
| SAML outbound SaaS SSO | provider-ready, real SaaS L4 missing | XID acts as the SAML/OIDC IdP for downstream SaaS. /sso/outbound/saml/:appId/metadata, /sso, /slo; the console OrgOutboundSso.tsx (including assignment gate editing) and the provider presets have landed; assertUserPassesAssignmentGate gates SSO launch; Slack/GitHub/Microsoft/Atlassian/Salesforce/Zoom templates already have local L3. Real SaaS admin L4 is still missing. Public docs MUST NOT promise production-supported |
| Enterprise legacy SSO | implemented, real gateway/L4 missing | LDAP POST /sso/ldap/:connectionId/login, WS-Fed GET | POST /sso/wsfed/:connectionId/\*, SWA POST /sso/swa/:connectionId/authenticateand the auth-gatedPOST /sso/swa/:connectionId/vault, header SSO POST /sso/header/:connectionId/authenticate(requiresattribute_mapping.\_legacy.trustedProxySecret), directory connector GET /sso/directory-connectors/typesandPOST /sso/directory-connectors/:connectionId/validate. Local fake LDAP/WS-Fed/SWA/header L3 is already covered; the production WS-Fed callback requires a signed wresult; real AD/LDAP gateway, AD FS WS-Fed, and Application Proxy L4 are still missing. Kerberos/IWA is a documented deployment mode only, and Workers does not terminate native Kerberos |
| Package | Status | Contract |
|---|---|---|
@xid-kit/core |
PASS | XidClient.signInPassword calls /auth/password/sign-in and refreshes /v1/me; password user creation is handled by the same method according to tenant policy; listApiKeys / createApiKey / revokeApiKey call /v1/api-keys; XidApiClient optionally takes secretKey and sends Authorization: Bearer sk_*; listUsers / getUser / listOrganizations / listSessions call the Management API and map snake_case to camelCase; the export lock is packages/core/src/__tests__/exports.test.ts |
@xid-kit/core active organization |
production L4 | XidApiClient.setActiveOrganization returns a lightweight ActiveOrganizationResponse; on success XidClient.setActiveOrganization clears the token cache and calls load() to re-read /v1/me. Focused mock tests, the local Worker cookie flow, and the production browser smoke test already cover this |
@xid-kit/react public surface |
partial production L4; full surface local PASS | The package entry exports XidProvider, 8 hooks, SignedIn/SignedOut/Protect, the hydration control components, AuthenticateWithRedirectCallback, RedirectToSignIn/RedirectToSignUp/RedirectToUserProfile/RedirectToOrganizationProfile/RedirectToCreateOrganization, the sign-in/out/up buttons, the UI components, and the organization components. packages/react/src/__tests__/exports.test.ts locks the complete public export surface to match docs/sdks/react.md. Production browser smoke covers only XidProvider, useAuth, useUser, useOrganization, useOrganizationList, SignedIn, SignedOut, and Protect; every other public export has local evidence only and MUST NOT be reported as production L4. A standalone useSignUp is no longer exported |
@xid-kit/backend public surface |
PASS | The package entry exports PACKAGE, verifyToken, authenticateRequest, verifyWebhook, toVerifyKeySet, JwksCache, AppError, BACKEND_ERROR_CODES, and their associated types. Access token verification accepts RFC 9068 typ=at+jwt and application/at+jwt. packages/backend/src/__tests__/exports.test.ts locks the public exports to match docs/sdks/backend.md |
@xid-kit/nextjs |
PASS | xidMiddleware, auth, getAuth, currentUser, xidClient, and the React/backend re-exports are the current package contract |
@xid-kit/react-native |
implemented, local evidence only | Hosted redirect, deep-link callback, state-keyed PKCE S256, and an injected secure-token-storage adapter are implemented with unit tests and typecheck. Real device storage, deep links, and a real-IdP round trip remain unverified, so this is not a production L4 claim |
| Flutter, iOS, Android, macOS SDK | implemented, local evidence only | Source packages and their platform flows exist and their documented local toolchain tests pass. Device/emulator-specific storage and browser behavior plus real-IdP round trips remain unverified; package presence and local tests MUST NOT be reported as production L4 |
| Area | Status | Contract |
|---|---|---|
NavLink active class |
local Console PASS | packages/web-ui/src/tanstack-router.tsx is compatible with the react-router className={({ isActive }) => ...} form and children render function. packages/web-ui/src/tanstack-router.test.tsx verifies that rendered anchors contain the resolved class and never contain function source |
Link href |
local Console PASS | The shared Link emits a real <a href> and takes over same-runtime SPA navigation with TanStack navigate on click. Focused tests cover dynamic string and object routes without __link__; links crossing the Core and Console Worker boundary retain their real href and use document navigation so the request host and host-only cookie remain intact |
The four OAuth protocol endpoints /introspect, /revoke, /device_authorization, and /register return errors in the RFC shape {error, error_description}, constructed directly inside the endpoint and never routed through the global onError. They MUST NOT return the XidAPIError {code, message} shape.
Error shapes are split by endpoint family:
-
Protocol endpoints (RFC family): the OIDC/OAuth endpoints
/authorize,/token,/userinfo,/introspect,/revoke,/device_authorization,/register,/par,/backchannel_authentication,/end_sessionand the like return{error, error_description}(RFC6749 5.2 plus the per-RFC extension codes). Client authentication failure is a 401 carryingWWW-Authenticate(Basic client:Basic realm="xid", error="invalid_client"; DCR management RAT:Bearer realm="xid", error="invalid_client");/registeruses the RFC7591 codesinvalid_redirect_uri/invalid_client_metadata/invalid_software_statement. -
Management API and Hosted UI endpoints (
/v1/**,/auth/**): errors are XidAPIError{code, message, meta?}, that is, each of the entries below. -
SCIM endpoints (
/scim/**): errors use the scimError shape (application/scim+json), and an unmatched path 404 uses the same shape. -
Boundary errors all use
AppError. -
A missing cookie session returns
401andcode = unauthorized. -
Insufficient platform permission returns
403andcode = forbidden. -
A nonexistent resource returns
404and MUST NOT leak cross-tenant resource existence. -
An input field error returns
422andcode = validation_failed, and whenmeta.paramNameis required the field name MUST be explicit.