IntelliAuth Documentation Audit
One-line state: a feature-rich identity platform whose docs are internally at war with themselves — class names, token claims, scope grammar, plan tiers, webhook formats, and even whether a Control Plane API exists all contradict across pages, and the canonical copy-paste values point at a non-routable .local domain.
1. Webhook signature verifier rejects every delivery the platform sends (critical)
Location: /developer/concepts/webhooks/ and /developer/webhooks/verify-signature/
Problem: The webhooks overview shows the timestamp header as an ISO-8601 string — X-IntelliAuth-Timestamp: 2026-05-17T12:34:56Z — while the verify-signature page declares it X-IntelliAuth-Timestamp: <unix-seconds> and parses it as an integer: const ts = Number(timestamp); if (!Number.isFinite(ts)) return false. Number("2026-05-17T12:34:56Z") is NaN, which fails the Number.isFinite guard.
Consequence: A developer who follows both pages — receiving the ISO timestamp the platform documents it sends, then running the official verification snippet — will reject 100% of valid webhook deliveries as forged. This silently breaks every event-driven integration (user.signed_in, user.mfa_enrolled, etc.) with no obvious cause.
The fix: Pick one wire format. If the platform actually sends Unix seconds, correct the overview example; if it sends ISO-8601, rewrite the verification samples to Math.floor(Date.parse(timestamp)/1000). Add a literal sample header value that matches the parser on both pages.
2. "There is no Control Plane API" vs. a fully documented Control Plane API (critical)
Location: /developer/reference/api/cp/overview/ vs. /cp-admin/reference/api/ (and /cp-admin/tenants/create/)
Problem: The developer-facing overview states the control plane is "not an integrator REST surface" and is managed "by humans, on the web," advising "If you find yourself wishing for a CP API, the right next step is to talk to the IntelliAuth team." The CP-admin reference says the opposite: "The control plane is driven by the same HTTP API the cp-org-console uses. If you're automating tenant provisioning... this is the surface you'll be talking to," complete with Base URL: https://manage.<your-domain>/api/v1 and POST /api/v1/organizations/{org_id}/tenants. The tenant-create page ships working @intelliauth/control-plane-sdk and curl examples against it.
Consequence: Developers cannot tell whether automation is supported. Half the docs tell them to give up and email sales; the other half hand them endpoints and an SDK. Anyone building tenant-provisioning automation has no idea if they're on a supported path or one that will be pulled.
The fix: State a single policy. If the CP API exists and is callable, retract the "not for calling" framing and link the two pages. If it's internal-only, mark the CP-admin reference and the control-plane-sdk examples as unsupported/internal.
3. Canonical quickstart values point at a non-routable .local domain (critical)
Location: /developer/getting-started/quickstart-react/, /developer/reference/sdk/node/management-client/, /developer/concepts/audiences-and-scopes/, and others
Problem: The primary React quickstart presents https://banking-cymmetri.intelliauth.local as the example tenant URL ("An IntelliAuth tenant URL (something like https://banking-cymmetri.intelliauth.local)") and hardcodes it into <IntelliAuthProvider tenantUrl="https://banking-cymmetri.intelliauth.local" ...>. The same .local host recurs in the SDK reference, the audiences/scopes JWT examples, and the signup wizard. .local is reserved for mDNS and does not resolve on the public internet.
Consequence: An agent or developer copy-pasting the quickstart gets a config that cannot connect to anything, with a DNS-level failure that looks nothing like an auth problem. It also leaks that these examples were lifted from a local dev cluster rather than written as docs.
The fix: Replace every intelliauth.local occurrence with a clearly-marked placeholder (https://banking-cymmetri.example.com or <tenant>-<org>.<your-domain>) and never present .local as "a real value."
4. Node quickstart uses a class name and constructor that the SDK reference contradicts (critical)
Location: /developer/getting-started/quickstart-node/ vs. /developer/reference/sdk/node/management-client/
Problem: The quickstart imports and constructs ManagementClient with only tenantUrl, clientId, clientSecret. The SDK reference names the same client IntelliAuthManagement and marks audience as a required constructor option (| audience | yes | — | The API audience the access token is for. |). So the quickstart both uses a class name that may not exist and omits a required field.
Consequence: Copy the quickstart and you get either an import error (ManagementClient not exported) or a runtime failure for the missing required audience. The two pages cannot both be right about the public API.
The fix: Reconcile the class name and the constructor contract in one place, then make the quickstart import the real class and pass every required option.
5. CLI/SDK/curl examples use a plan tier (growth) that does not exist (critical)
Location: /cp-admin/tenants/create/ vs. /cp-admin/plans/tiers/
Problem: Tenant-create uses --plan growth in the CLI, plan: 'growth' in the SDK, and the same in curl. The canonical tiers page says "Four tiers ship today: Free, Starter, Pro, Enterprise" — there is no growth.
Consequence: Every provisioning example fails (or silently mis-provisions) because it names a nonexistent plan. This is the single most-copied operation in the CP docs.
The fix: Change all growth examples to a real tier (e.g. pro) and add a test that rejects example plan values not present in the tiers table.
6. Access-token lifetime is documented as both 5 minutes and 60 minutes (significant)
Location: /developer/concepts/sessions-and-tokens/ vs. /developer/oauth/refresh-token-rotation/
Problem: Sessions-and-tokens says the access token is "short-lived (typically 5-15 minutes)" with the diagram showing access_token_1 (exp 5min). The refresh-rotation page returns "expires_in": 3600 (60 minutes), says the SDK refreshes "About 60 seconds before expiry," and warns you "do not want to send the user back through /oauth2/authorize every hour" — clearly assuming a 1-hour token.
Consequence: Developers sizing cache TTLs, retry windows, and step-up max_age logic against the token lifetime will be wrong by 4–12x. API-side validation tuned for 5-minute tokens will behave unexpectedly against 60-minute ones.
The fix: State one default lifetime, make expires_in in the example match it, and align the diagram and prose.
7. The AAL claim has two different names and two AAL3 definitions (significant)
Location: /developer/concepts/authentication-levels/ and /developer/concepts/sessions-and-tokens/ vs. /developer/mfa/step-up/
Problem: The concepts pages put an auth_level claim on the access token ("auth_level": "AAL2", enforced as req.user.auth_level). The step-up page uses a different claim entirely — acr (lowercase 'aal2') plus auth_time — and its enforcement reads if (token.acr !== 'aal2' || tokenAge > 300). AAL3 is also defined twice: "WebAuthn / passkey / hardware security key" (concepts) vs. "phishing-resistant MFA and a hardware-backed key, all in a recent window" (step-up). Casing also varies: AAL2 vs aal2 vs AAL 2.
Consequence: A developer who reads concepts and writes req.user.auth_level will find that claim absent on step-up tokens (which carry acr), so step-up enforcement silently fails open or always 401s. Agents parsing claim names will pick one and break against the other.
The fix: Choose one claim name and value casing (OIDC convention is acr), update every example and enforcement snippet, and write one canonical AAL3 definition.
8. Scope grammar is inverted between two reference pages (significant)
Location: /developer/concepts/audiences-and-scopes/ vs. /developer/reference/sdk/node/management-client/
Problem: The concepts page establishes scopes as action:resource — read:transactions, write:settings, manage:applications. The management-client reference uses the opposite order, resource:action — scope: 'users:read users:write groups:read groups:write audit:read'.
Consequence: Scope strings are exact-match. A developer following the documented grammar will request scopes the server doesn't recognize, getting authorization failures that are hard to diagnose because both forms "look right."
The fix: Pick one grammar, document it once as canonical, and rewrite the non-conforming examples.
9. Site-wide "All tenant admin docs" nav link 404s (significant)
Location: Global footer/nav → /admin/getting-started/welcome/
Problem: Every page's footer/nav links "All tenant admin docs" to /admin/getting-started/welcome/, which returns HTTP 404. The real landing page is /tenant-admin/getting-started/welcome/.
Consequence: A primary navigation entry present on every page is dead, dropping users on a 404 instead of the tenant-admin section. Crawlers and doc-indexing agents record the entire section as unreachable from nav.
The fix: Repoint the global link to /tenant-admin/getting-started/welcome/ (or add a redirect) and add a link-check to CI.
10. Changelog returns 403, but deprecation policy depends on it (significant)
Location: Global footer/nav → /changelog/
Problem: /changelog/ returns HTTP 403 Forbidden (page title literally "403 Forbidden"), yet it's linked in the global footer/nav and the CP API reference promises "Deprecations are announced in the changelog with at least 6 months of overlap."
Consequence: The one resource developers are told to watch for breaking changes is inaccessible. There is no way to honor the deprecation contract the docs advertise.
The fix: Make /changelog/ publicly readable (or remove the link and the promise until it is), and verify access in CI.
11. Two different GitHub orgs for official code (significant)
Location: /developer/getting-started/sample-apps/ vs. global footer
Problem: The sample-apps clone URL is https://github.com/intelliauth-co/examples.git (org intelliauth-co), while the global footer GitHub link points to https://github.com/intelliauth (org intelliauth).
Consequence: Developers can't tell which org is canonical; one of the two is likely wrong or unowned, which is a supply-chain and trust hazard (cloning from the wrong org). Agents resolving "the official repo" pick one at random.
The fix: Consolidate on one org, fix the other reference, and confirm both the clone URL and the footer link resolve.
12. useIntelliAuth() overstates its surface and references an undocumented hook (significant)
Location: /developer/reference/sdk/react/use-intelli-auth/
Problem: The page claims "25+ imperative methods" / "around 25 fields," but the Returns table lists only ~11 fields and omits every MFA/passkey/step-up method referenced elsewhere (startMfaEnrolment, loginWithPasskey, onMfaRequired, prepareMfaChallenge, stepUp). The stepUp() used on the step-up page isn't in this table at all. The same five error codes appear in two near-identical tables, and the page points to useIntelliAuthSignUp(), which is documented nowhere in the React SDK nav.
Consequence: The "complete" hook reference is neither complete nor accurate. Developers can't find the methods they're told to use (stepUp, MFA enrollment), and are sent to a hook (useIntelliAuthSignUp()) that has no documentation.
The fix: Make the count match the table, add the missing MFA/step-up methods (or remove the "25+" claim), deduplicate the error table, and either document useIntelliAuthSignUp() or stop referencing it.
13. Docs warn against uppercase tenant names, then the figure uses one (significant)
Location: /cp-admin/tenants/create/
Problem: A callout documents a real saga bug: names starting with an uppercase letter "like Production" pass form validation but "get rejected deeper in the saga — the platform marks the tenant Active, but the welcome email never reaches the first admin and the tenant admin console can't sign anyone in." Then Figure 2 demonstrates a "Production" (capitalized) tenant — the exact failing input the callout warns against.
Consequence: An operator following the figure creates a tenant that looks Active but is permanently unusable (no admin email, no console sign-in), reproducing a known data-corruption footgun the docs explicitly flagged.
The fix: Change the figure to a lowercase-dashed slug (e.g. production), and ideally note that the validation gap is being fixed.
14. "Three-step signup wizard" actually has four steps, and the page leaks a dev-only footgun (significant)
Location: /cp-admin/getting-started/sign-up-and-create-org/
Problem: The page is titled a "Three-step signup wizard" and says "The signup form is three steps," but documents Step 1, Step 2, Step 3, and Step 4 — Verify your email. It also exposes an internal env var, INTELLIAUTH_MAIL_DEV_ALLOWLIST, and a silent-failure mode: "If your address isn't on it, the send is silently dropped and you'll never receive the link — even though the page tells you it was sent."
Consequence: The step count is wrong, and the silent email-drop behavior (gated by an internal allowlist var) appears in public docs with no resolution path — new users hit "Check your email," get nothing, and have no way to know why.
The fix: Correct the step count, and move dev-cluster behavior into a clearly-scoped "local development" note (or out of public docs), with explicit guidance on how to recover when no email arrives.
15. Tenant-admin docs say "no REST API," but a documented SDK exposes exactly that (significant)
Location: /tenant-admin/getting-started/welcome/ vs. /developer/getting-started/quickstart-node/ and /developer/reference/sdk/node/management-client/
Problem: Tenant-admin welcome asserts "There is no REST API for tenant admin operations. This is deliberate." Yet the Node management client exposes programmatic users, groups, applications, and audit operations (e.g. mgmt.users.get(...), mgmt.applications.list(...)) — tenant-admin-equivalent actions against the data-plane REST surface.
Consequence: Developers reading the tenant-admin section conclude automation is impossible and build manual workflows, unaware the Management SDK already does it. Conversely, those using the SDK are told it "doesn't exist."
The fix: Reconcile the boundary: clarify that tenant console operations are web-only but the Management API covers users/groups/apps/audit programmatically, and cross-link the two.
16. Tenant-side advertises custom roles; control-plane forbids them (significant)
Location: /cp-admin/reference/roles-reference/ vs. /tenant-admin/roles/custom-role/
Problem: The CP roles reference is explicit: "You can't currently define custom roles; the three below are the entire vocabulary" (Owner/Admin/Viewer). The tenant-admin section advertises a custom-role capability (/tenant-admin/roles/custom-role/, alongside system-roles/).
Consequence: Readers can't tell whether custom roles are supported. Someone planning an RBAC model at the org level builds around three fixed roles; someone at the tenant level designs custom roles — and they may collide at integration time.
The fix: State the scope of custom roles explicitly (tenant-level only, if true) on both pages so the CP "no custom roles" rule doesn't read as platform-wide.
17. Provisioning phases and event catalogue disagree across three pages (significant)
Location: /cp-admin/provisioning/overview/, /cp-admin/manuals/stuck-provisioning/, and /cp-admin/audit/event-reference/
Problem: The provisioning overview lists five phases ending in "Send the welcome emails." The stuck-provisioning runbook lists a different six-step sequence — "validate input → allocate resources → seed schema → deploy workloads → wait for ready → mark active" — with no welcome-email step and added "seed schema"/"wait for ready" steps. The event catalogues also diverge: the audit event reference adds step_failed, compensating, and documents tenant.provisioning.cancelled as "documented here as the canonical event... but the saga doesn't emit it yet," while the overview omits all three.
Consequence: Operators debugging a stuck saga can't map the runbook's steps to the overview's phases, and anyone building monitoring on provisioning events will subscribe to tenant.provisioning.cancelled (documented as canonical) that never fires — a silent gap in incident detection.
The fix: Publish one canonical phase list and one event catalogue, reference them from every page, and clearly tag not-yet-emitted events (cancelled) as unavailable rather than canonical.
18. Developer welcome lists WebAuthn and SAML as if always available; both are plan-gated (significant)
Location: /developer/getting-started/welcome/ vs. /cp-admin/plans/tiers/
Problem: The developer welcome says users can "sign in with email + password, social providers, WebAuthn, SAML, or whatever you've enabled." The tiers page gates "WebAuthn / passkeys" to Starter+ and "SAML federation" to Pro+ (both show — on Free). The sample-apps SAML example likewise assumes SAML availability.
Consequence: A developer on Free builds against WebAuthn or SAML following the welcome page, then discovers at runtime the feature isn't enabled for their tier — wasted integration effort and a confusing "why doesn't this work" moment.
The fix: Annotate plan-gated auth methods inline on the developer welcome (e.g. "WebAuthn — Starter+, SAML — Pro+") and link the tiers table.
19. The org/tenant/app hierarchy is described as three layers and four layers on the same path (minor)
Location: Homepage and /developer/concepts/tenants-and-orgs/
Problem: The homepage card calls it the "four-layer hierarchy." The concepts page's <title> is "Organisation, tenant, application" (three), its meta description says "The three-layer hierarchy," and its body says "IntelliAuth structures the world in three layers" under a heading "The three layers (and a fourth)" — then diagrams four (Organisation → Tenant → Application → Identity).
Consequence: Three different counts for the foundational mental model on the "read this first" page erodes confidence and confuses agents trying to extract the canonical structure.
The fix: Decide whether Identity is a layer of the hierarchy or a separate concept, then make the homepage card, title, meta, heading, and body agree.
20. CP API auth advice tells users to reuse a browser session token (minor)
Location: /cp-admin/reference/api/
Problem: With service accounts "coming when service accounts ship," the documented auth path is: "Re-use a session token captured from a logged-in browser session (works for ad-hoc scripts, not for production automation)." The page also lists a RATE_LIMITED/429 error pointing to "Plans → Rate limits," but that page says the control plane API is "not rate-limited," and the OpenAPI/Swagger download is roadmap-only.
Consequence: Reusing browser session tokens for scripting encourages credential handling that's fragile and a security smell, with no machine-readable spec to script against and a documented error code (RATE_LIMITED) that the rate-limits page says can't occur.
The fix: Mark the browser-token approach as unsupported/temporary with an explicit "do not do this in production" warning, reconcile the RATE_LIMITED error against the no-rate-limit statement, and ship the OpenAPI spec so agents can discover endpoints.
21. Minor naming/format inconsistencies that break exact-match parsing (minor)
Location: Multiple — see below
Problem: Several smaller mismatches will trip exact-match consumers: refresh failure surfaces as OAuth invalid_grant (refresh-rotation page) but the hook reference uses session_expired; audit event shape uses type + actor_id (event reference) vs. event_type + actor.id (management client); the tenant-lifecycle body documents six states while the page's own meta description names only four; cacheLocation: 'localstorage' is referenced on the refresh-rotation page but isn't a documented provider prop in the quickstart/concepts; and domain placeholders vary across <DOMAIN_BASE>, <domain>, <your-domain>, plus hardcoded .com/.local.
Consequence: Each mismatch is small alone, but together they mean error-handling code, audit-log parsers, and config built from one page silently fails against another — exactly the class of bug humans paper over and agents can't.
The fix: Standardize one error-code vocabulary, one event-shape schema, one placeholder token, and document every referenced provider option (cacheLocation). Align the lifecycle meta description with the six-state body.
What they do well
- Concepts are genuinely well-explained — the applications-and-clients page maps four OAuth client types to real frameworks, and the tenant-lifecycle transition table is a clear, correct state machine.
- They document their own footguns — the uppercase-tenant-name saga bug and the local email-drop allowlist show unusual honesty about real failure modes (the problem is letting examples then trip those footguns).
- OAuth 2.1 + OIDC + PKCE coverage is thorough, with sensible client-type-to-flow guidance and explicit M2M/client-credentials handling.
Top 3 recommendations
- Run a contradiction sweep across the SDK and token contract — class name (
ManagementClientvsIntelliAuthManagement), requiredaudience, scope grammar (read:transactionsvsusers:read), AAL claim (auth_levelvsacr), and token lifetime (5min vs 3600). These break copy-paste integration and step-up enforcement today. - Fix the verifier and the webhook timestamp format — pick ISO-8601 or Unix seconds, make the example header and the parser agree, so the official signature check stops rejecting every valid delivery.
- Add CI link-checking and a single source of truth for placeholders/plans/orgs — repair the site-wide
/admin/...404 and the/changelog/403, purgeintelliauth.localand the nonexistentgrowthtier from examples, and consolidate the two GitHub orgs.