switching clears the token & responses
No token

Auth Scenario Tester

Scenarios 1–58 below are the usage flows of iam-api-list.md §2 — numbered and titled to match it, so §2.14 in the doc is 14 here — and 59 is the platform bootstrap from iam-operational.md §1. Requests are sent directly from your browser to the Kong gateway (http://localhost:8080) — no app-side proxy — so you are exercising the real gateway↔IAM orchestration: klp-auth-guard verifies the access token, forward-auths to IAM /internal/validate, mints the x-gateway-context, and proxies to the IAM service. Because this is a cross-origin call, Kong must allow CORS from http://localhost:4300 (incl. credentials and the OPTIONS preflight) or the browser will block the response.

How to use

  1. Make sure Kong (:8080) and IAM (:3100) are up and the routes are loaded.
  2. Empty database? There is no operator yet and none can be created through the API — start at 59 Creating the genesis operator (Genesis, the last menu item) to seed the first operator out of band and bring it online. Everything under Operator Auth needs that account to exist first.
  3. Open a scenario with a login step — 2 / 3 / 9 / 10 / 14 (tenant) or 39 (operator) — and run it; the response's accessToken is captured automatically into the top Authorize bar.
  4. Protected/admin endpoints then send Authorization: Bearer automatically; the gateway turns that token into the verified context the IAM service consumes. Public endpoints (🌐) send no Bearer token — there is no token input on them.
  5. Fill path/query params and JSON bodies Swagger-style, then Send.
  6. Every card carries a Field guide (what each input means and where its value comes from) and a Response guide (every property that comes back and what each value implies) — open them instead of guessing.

Seeded smoke user: smoke.user@example.com / Password123! (admin in tenant A). Out-of-band values (email verify links and short codes, password resets, invitations, OTP/TOTP codes, login-challenge codes, step-up codes, credential-change codes, device and registration tokens) are never returned by the API — use the ⛓ out-of-band helper shown on the card that consumes them to mint one locally (dev only), then copy the value into the form. Cards that take an Idempotency-Key show a header input: pre-filled with a fresh UUID where the route requires one, empty (no header sent) where it is optional.

Four things to know before the sign-up and sign-in groups

  • Each front door opens with the scenario that renders it. 1 asks which sign-up methods are live, 7 which sign-in methods are. They are separate policies — one governs how an account may be created, the other how an account that already exists may authenticate — written from two separate operator scenarios (45 / 46), and they may disagree. A sign-in screen rendered from the registration policy will offer a method login refuses, or hide one it accepts.
  • Login takes an identifier — an email or an E.164 phone, discriminated by shape (scenarios 9 / 10). { email } still works as a deprecated alias, so nothing breaks. On a browser IAM already recognises the body needs only { password }: the device cookie names the account, which is what lets the “Welcome back” screen (8) avoid asking for an identifier it can only ever show masked.
  • A login may be a chain. POST /v1/auth/login can answer with a challenge instead of tokens, and each hop is cleared at /v1/auth/login/challenge/verify until the plan is exhausted (scenarios 11–13). The whole chain sits behind DEVICE_TRUST_ENABLED; with it off (the default) login behaves exactly as it always did and only 9 / 12 apply.
  • Link and short code are two halves of one challenge. Every out-of-band message (email verification, invitation) carries a long link and a 6-digit code on the same row — redeem either, and the other dies with it.

The single-hop /v1/auth/mfa/verify and /v1/auth/mfa/otp/request routes, and the one-shot /v1/auth/phone/register, are deprecated but fully working — rewired onto the same engine. They are shown where they still apply so existing clients can read their own flow, and mapped together with the retired routes in 37.

A 200 from login is a body to read

POST /v1/auth/login answers 200 five different ways, and only one of them is a sign-in. Branch on the body — accessToken, challengeToken, mfaRequired, noOrganization, invitationPending — before assuming the caller is signed in.

  • Belonging nowhere is not a 403 any more (15). A complete account with no membership of any kind — typically a member removed from their only organization — gets { noOrganization, bootstrapToken }: no session, no cookie, no access token, and a token that opens exactly one door, POST /v1/auth/organizations/bootstrap, which founds their organization and signs them into it. The signed-in counterpart is 24, where GET /v1/me → foundedTenantId says whether founding is even offerable: the cap is one live founded organization per identity.
  • An invitation you have not accepted is not an organization (51). When the only seats are invitations, login answers { invitationPending, acceptToken } instead — and that token is the proof an existing identity must present to POST /v1/auth/invitations/accept. The two tokens are not interchangeable. The login card shows either one in its capture line.

Every profile change starts with a step-up

§153 split profile editing into one route per field, and every one of them spends a single-use step-up token minted for one named action. A live session is never enough. The old combined routes — PATCH /v1/me, POST /v1/auth/password/change, POST /v1/me/phone and its OTP request, POST /v1/me/mfa/otp/request — now answer 410, and 37 maps each to its successor.

  • 🔐 The tester carries the token for you. POST /v1/me/step-up proves the current password and answers a challengeId and a password-assurance token; both are captured and filled into the cards below it. …/code sends a code bound to that challenge, …/verify proves a factor and returns a strong token that replaces the first, and the card that makes the change spends whichever is newest. Mint a new one per action — a token for mfa_disable will not regenerate recovery codes.
  • Password assurance is enough for some actions, never for others. A display name (28) and a password change (35) take the password token as-is. Changing the sign-in channel (33) and both MFA actions (27) need a strong one. The response's passwordOnlyAllowed says which case you are in — and an account with no eligible factor is handed an empty options list, which means password-only is the only route and the change will be held.
  • A contact change is a row, and a password-only one is held (29–32). A code goes to the new address or number while the current one keeps signing in. A strong token applies the change when its code is confirmed; a password-only one is held three days, and every notice carries a cancel link that works without a session — because the email is where password recovery goes, and a stolen session must not be able to point it elsewhere for good. 36 is the view across all live changes, and the way to stop them signed in or signed out.
  • Each add is paired with its change. Linking a first email (29) and changing one you hold (30) are the same route, as are linking a first phone (31) and replacing it (32) — what routes you is email: null / phone: null on GET /v1/me. The differences all follow from whether there is an old value to protect: no old address means no current value factor to prove, no 422 unchanged to hit, and the notice goes to the other channel instead.
  • Two new ⛓ helpers. “Get a step-up code” reads the code bound to the open challenge (it is pre-filled with the captured challengeId), and “Get a credential-change code” reads the code of your live email / phone / password change (pre-filled with your userId). The sessionless cancel link has no helper — its token exists only in the notice.

One session, many apps

Seven scenarios — 17, 18, 20, 47, 48, 56 and 57 — cover the multi-app surface. In the first six, every route sits behind MULTI_APP_ENABLED and answers 404 when it is off; they carry a ⚑ badge in the menu so a 404 there reads as the flag rather than a wrong path. 57 carries none, because the doc gates its two join routes behind no flag.

  • Three separate acts, deliberately. Registering an app (platform configuration, one row serving every tenant) is not enabling a tenant for it (access control, per tenant) — both live in 47 — and neither grants any individual anything, which is 56. Roles say what a member may do inside an app; grants say which apps they may enter at all.
  • …and one act the member performs themselves (57). An app the operator marked is_auto_enabled_on_register is free to acquire at sign-up — a visitor with no account gets a tenant, an enablement and a grant in one transaction just by filling in the registration page. Someone who already has an account, opening that same page, is refused 409 …_already_registered, and had no other route: 57 is that route, and gates on the same registry predicates the registration hand-off uses, refusals included. It is the only app-surface scenario with no ⚑ badge — the doc puts /v1/me/app-join-targets and /v1/me/app-joins behind no flag; only the drop-down check at its last step is behind MULTI_APP_ENABLED. It is not the paid subscription path: an app the operator has not opted in is refused, and reaching it means subscribing.
  • A join is not finished until you switch (57). A session carries one tenant. Join into organization B while signed in as A, redirect without switching, and the app receives a token scoped to A — which has no enablement for it — so the caller meets the very refusal they came to escape, having just successfully joined. That is why switch-tenant has its own card in 57 rather than a footnote, and why the join response returns a tenantId even for an organization it has just founded.
  • A granted member who is missing downstream is now an anomaly. Granting (56) projects the member at the request — user.upserted + membership.upserted in the same transaction, and a revoke emits membership.removed for that app alone. That closed the gap in which a member granted after their tenant's backfill passed every /internal/validate hop while being absent from the app's own copy. So if one is missing now, suspect an undrained outbox or a failed delivery; the diagnosis and the re-sync are 48. (The doc's §3.96 / §3.97 still describe the old gap; §2.47 and the service agree it is closed, and the cards say so.)
  • Model A and Model B sit side by side. 16 is the older shared-login probe (OAUTH_ENABLED): the app asks IAM to validate a return URL and IAM renders the login. 17 supersedes it — a standards OAuth 2.1 authorization request returning the app's own app-scoped tokens. Different flags, rolled out independently.
  • Authorize is not a fetch call. GET /v1/oauth/authorize answers 302 and carries no CORS headers under any circumstances, because OAuth 2.1 reaches it by navigation. Its card opens the URL in a new tab and takes the redirect back by paste. The 🔑 PKCE control mints the verifier/challenge pair and fills the verifier into the token exchange, because a mismatch there answers a deliberately undifferentiated 400 invalid_grant.
  • N chains, one session. 19 renews the session; 20 is what lets two products stay open in two tabs without either signing the other out. Each app rotates on its own /v1/auth/apps/{clientId}/refresh route — and /v1/auth/refresh answers 404 wherever the flag is on. The chains are isolated; the session is not. Switching tenant (21) repoints every live chain at once, and revoking the session (22) kills all of them — there is no per-app logout.
  • The two CORS preflights have no cards, on purpose. OPTIONS /v1/oauth/token (§3.90a) and OPTIONS /v1/auth/apps/{clientId}/refresh (§3.92a) are issued by the browser, never by application code, so every Send on the token and app-refresh cards already exercises them — and a hand-rolled fetch of one could only ever be blocked. They differ in granularity because of what a preflight can identify: the refresh route names the client in the path, so its allow-list is genuinely per-client and refuses another registered client's origin; the token route carries the client id in the body, so its allow-list is registry-wide and the per-client binding is enforced by the code binding and PKCE instead.

One invitation, five scenarios

50–54 follow one invitation in the order it actually travels: sent by email (50) or by phone (51), accepted from the message by someone who already has an account (52) or from inside a live session (53), and re-delivered when it never arrived (54). Each of the last three exists because a plausible-looking shortcut is wrong.

  • Ask, then prove, before you accept (52). One identifier is one identity across every tenant, so POST /v1/invitations reuses an existing account rather than duplicating it — and the invitee therefore arrives at the accept screen in one of two entirely different situations. POST /v1/auth/invitations/describe answers which, without consuming the invitation, so password is sent only when the account has no credential yet. An existing identity must then sign in first and present that proof — the acceptToken from an invitationPending login, or its own session — or accept answers 401 invitation_requires_signin. A tenant-scoped invitation used to act as a global password reset for any address an admin could name; now it writes nothing global at all.
  • Signed in? Accept from the list (53). An invitation shows up in GET /v1/me/memberships as a status: invited row. Render it as “accept”, never “switch” — switching to an invited seat is 403. POST /v1/me/memberships/{tenantId}/accept takes no token: the gateway context is the identity, and the row is the evidence of the invite.
  • Resend, don't re-invite (54). POST /v1/invitations answers 409 while an open invitation exists — even a lapsed one, because the open-invite index is partial on used_at IS NULL and cannot consider expiry. POST /v1/invitations/{userId}/resend is the only path that helps during the link's 7 days: it consumes the open row (both old secrets die) and mints a fresh link + code on the invitation's original channel. Mint the ⛓ helper value after the resend — anything copied before it is already dead.

How the scenarios are grouped

  • Tenant Auth (1–38) — the public, tenant-facing profile: signing up, signing in, sessions & devices, organizations, MFA, profile & credentials, legacy clients, and the platform discovery surface. Sub-headings follow the doc's own purpose clusters, so counterparts sit adjacent: signing in with no organization (15) and founding one while signed in (24), enrol-MFA next to disable-MFA, each add next to its change (29/30, 31/32), sessions next to devices, one app (19) next to many (20). The doc's 2026-09-27 revision folded its old passwords cluster into profile & credentials, so the reset (34) and the signed-in change (35) sit with the other credential flows.
  • Operator Auth (39–49) — the internal control plane: operator sign-in (MFA mandatory, no device trust), operator & role administration, tenant lifecycle, act-as, the cross-tenant audit read, the two global policy writes — registration (45) and login (46), separate levers on purpose, since retiring a channel usually means editing both — the app registry (47) next to diagnosing an app that is missing a member (48), and internal ops.
  • Both (50–58) — tenant-admin flows an operator also reaches via act-as. The invitation half is five scenarios (50–54, above), then member/role administration (55). The app half splits the same dimension two ways — an admin deciding which apps a member may enter (56) and a member acquiring a free-to-acquire app for an organization they administer (57) — and the tenant audit log (58) records both.
  • Genesis (59) — platform bootstrap. Outside §2 (it is iam-operational.md §1) and deliberately last, because you only ever run it once per platform.

Signing up

1Rendering the right sign-up formBefore showing any sign-up UI, the client asks which registration methods are live so it renders an email form, a phone form, or both.1 endpoint(s)2Signing up with emailA new user signs up with email + password, which also creates their organization (tenant) and makes them its founding admin. Email verification is required before the first login — and verification itself now ends in a session.4 endpoint(s)3Signing up with a phone numberA new user signs up with a phone number; this also creates their tenant + founding admin (full parity with email sign-up). Since Enhancement 4 it is a three-step flow that ends in a real password — the account is no longer password-less.4 endpoint(s)4Signing up with email + phone (link mode)A user wants both an email/password login and a phone login. Under combinationMode=link (the default) this is a two-step journey: sign up one way, then add the other credential. The add is a PHONE CHANGE (scenario 31) — a step-up token, a code to the new number — and is independent of the sign-up policy.9 endpoint(s)5Signing up with email + phone (atomic mode)A single sign-up that supplies and verifies BOTH credentials at once: the phone proven inline by its OTP, the email proven out-of-band afterwards. Routed here when the operator has set combinationMode=atomic and allowedMethods holds both methods.4 endpoint(s)6Resuming an interrupted sign-upA user closed the tab, lost the SMS, or came back days later. They re-enter only the email or phone they started with, and the API says which step to render — for every sign-up path above and for a pending invitation.2 endpoint(s)

Signing in

7Rendering the right sign-in formThe mirror of scenario 1, for the other half of the front door: before showing any sign-in UI the client asks which LOGIN methods are live, so it offers a password form, a one-time-code form, or both. A different question from scenario 1 — that policy governs how an account may be CREATED, this one how an account that already EXISTS may authenticate.1 endpoint(s)8Recognising a returning browserBefore any credential is typed, the login screen asks whether THIS browser has signed in here before, so it can render “Welcome back, Budi” instead of a blank form. Behind DEVICE_TRUST_ENABLED; always { known: false } when off.2 endpoint(s)9Logging in with email & passwordThe base login: email + password, on a browser this IAM already recognises, with no second factor enrolled (the LE2 path). Whether MFA or a device challenge applies is not something the client can check beforehand — the login response itself reveals the branch.1 endpoint(s)10Logging in with a phone & passwordA phone-first account (from scenario 3) signs in with its NUMBER and password — the LE1 path. New in Enhancement 4: before it, the phone path never checked a password and the password path never accepted a phone.1 endpoint(s)11Logging in on a new deviceThe password was right, but this browser has never signed in here before, so IAM adds exactly ONE challenge — delivered on the account's primary channel (LN1 phone mode / LN2 email mode). Routed here when DEVICE_TRUST_ENABLED is on and no valid device_token cookie was presented.3 endpoint(s)12Logging in with a second factorThe account has an MFA method enrolled, so login returns a challenge instead of tokens — even on a recognised device (LE3). A recognised device never skips the MFA hop: device trust is a convenience, an enrolled second factor is a commitment.4 endpoint(s)13Logging in on a new device with a second factorThe hardest case (LN3): a new browser AND an enrolled MFA method, so the user clears TWO challenges in sequence. This is the reason the single-hop mfaToken was replaced by a chain.4 endpoint(s)14Logging in with a phone (one-time code)An existing phone user signs in with an OTP and NO password at all — primary phone authentication, distinct from the phone-OTP second factor (scenario 12) and from phone + password (scenario 10). Deliberately untouched by Enhancement 4 AS AN AUTHENTICATION FLOW: no device challenge, and clearing it earns no device trust; amr stays [sms]. The one exception is about ADDRESSING, not authentication — a device_token cookie may supply the destination so the number can be omitted.2 endpoint(s)15Signing in when you belong to no organizationA complete account — a verified credential and a password — that holds NO membership of any kind can authenticate, yet has nowhere for a session to live; typically a member removed from their only organization. Sign-in answers a purpose-scoped bootstrapToken instead of tokens, and the caller founds their own organization with it. Routed here by the BODY of the sign-in answer (noOrganization: true), not by a setting. The signed-in counterpart is scenario 24. To reach the state: remove a member from their only tenant (scenario 55), then sign in as them.3 endpoint(s)16Entering a partner app (cross-app login)A relying app (e.g. web-it-cms) sends the user to IAM to sign in and wants them bounced back afterwards. IAM validates the return URL against the registered client BEFORE rendering anything, which is where the open-redirect hole is closed. Behind OAUTH_ENABLED — the route 404s when off. THIS IS MODEL A, AND SCENARIO 16 SUPERSEDES IT: here the app asks IAM to validate a URL and IAM renders the login; there, the app sends a standards OAuth 2.1 authorization request and receives its OWN app-scoped tokens rather than riding the shared session. The two are gated by different flags (OAUTH_ENABLED here, MULTI_APP_ENABLED there) and can be rolled out independently, but an app that has moved to Model B has no use for this probe.2 endpoint(s)17Signing in through an app's authorization request (OAuth 2.1)The full Model B front door, and the successor to scenario 16: the app does not ask IAM to validate a URL and then render its own login — it sends a standards OAuth 2.1 authorization request and receives a code it redeems for its OWN app-scoped tokens. The app must already be registered (scenario 47) and granted to the member (scenario 56).4 endpoint(s)MULTI_APP_ENABLED18Switching apps from the drop-down (hand-off)The in-product counterpart of scenario 17: an already-signed-in user picks another app from the drop-down and lands in it signed in, without a round trip through a login screen.3 endpoint(s)MULTI_APP_ENABLED

Sessions & devices

Multi-factor authentication

Profile & credentials

28Changing your display nameThe lightest profile change: one step-up on the password, one call. One field, one endpoint, one step-up token — the retired PATCH /v1/me is mapped in scenario 37.2 endpoint(s)29Adding an email credentialA signed-in user who has NO email links one — typically a phone-first identity from scenario 3. It is the SAME route as an email change (scenario 30), and the differences all follow from there being no old address to protect. Routed here by email: null on GET /v1/me.8 endpoint(s)30Changing your email addressA step-up, then a change ROW: a code goes to the new address while the CURRENT verified email keeps signing in — so a typo never locks the account out. A STRONG token applies the change the moment its code is confirmed; a password-only one is HELD three days first, with a sessionless cancel link in every notice — because the email is where password recovery goes, and a stolen session must not be able to point it elsewhere for good.8 endpoint(s)31Adding a phone credentialA signed-in user who has NO phone links one, so it can carry a sign-in code, a second factor, or become the primary channel. Routed here by phone: null on GET /v1/me. Replacing a number you already hold is the same route with different consequences (scenario 32). Not policy-gated.8 endpoint(s)32Changing your phone numberA signed-in user replaces the verified number they already hold — a new handset, a new operator, a number that is no longer theirs. Routed here by a non-null phone on GET /v1/me; linking a first number is scenario 31. The email twin is scenario 30.8 endpoint(s)33Changing your primary sign-in channelAn account holding BOTH an email and a verified phone chooses which one is its primary channel. That choice decides which sign-in form applies (scenario 9 vs 10) and, more importantly, which channel a new-device challenge is delivered on. Gated by a STRONG step-up since §153.5 endpoint(s)34Resetting a forgotten passwordStart a reset from an email OR an E.164 phone (BR-8), redeem the emailed token or the SMS code to set a new password, then log in. The reset revokes every session and every device trust — and then re-trusts the browser that performed it (BR-9), so this machine is not challenged afterwards.3 endpoint(s)35Changing your password while signed inSince §153 the current password never travels in the change body: a step-up proves it, the new password is HELD on a change row while a code goes to your sign-in channel, and confirming the code applies it — ending every session (the current one included) but keeping this device trusted.6 endpoint(s)36Reviewing and cancelling pending credential changesEmail, phone and password changes (scenarios 30, 31, 35) each leave a change row that lives until it is confirmed, applied, cancelled or expires. This is the view across all of them — what is in flight, what happened before, and how to stop it, including from a notice when you are NOT signed in.4 endpoint(s)
39Signing in (operator authentication)Operators authenticate via /v1/admin/auth/* over the internal-only ingress, with MFA mandatory (TOTP — the tenant-facing phone-OTP method does not apply). The operator access token is minted here (aud: iam-platform); the gateway then verifies it to mint the x-gateway-context every other /v1/admin/* route consumes. Device trust does NOT apply to operators — by decision, not omission: a mandatory second factor on every login is the control, so sign-in is identical on a familiar and an unfamiliar machine whatever DEVICE_TRUST_ENABLED is set to.6 endpoint(s)40Onboard an operator and manage rolesList existing operators, create one (setup token out-of-band), grant a time-bounded role with step-up, confirm, review grants, and later revoke. Requires iam-platform-admin throughout — so the VERY FIRST operator cannot be created here: it is seeded once, out of band (scenario 59).6 endpoint(s)41Provision a tenantReview existing tenants, create a tenant + founding-admin membership in one audited transaction (single-use 7-day invite out-of-band), then inspect it. Requires tenant-lifecycle-operator.4 endpoint(s)42Decommission a tenant (suspend → soft-delete → hard-delete)Suspend → soft-delete → hard-delete. Hard delete is two-phase and four-eyes. Requires tenant-lifecycle-operator. Restore is the counterpart of suspend — and the only lifecycle action that needs no step-up.4 endpoint(s)43Support acts inside a tenant (act-as)Spans both profiles. Mint a short-lived, tenant-scoped token on the control plane — the cross-tenant decision is made once, here: audited, time-bounded, reason-bound — then use it against the public surface. Requires support-acting-in-tenant.4 endpoint(s)44Investigate with the audit log (cross-tenant)The canonical cross-tenant forensic read. Allow-listed filters only; cursor-paginated. Requires security-investigator. ip / userAgent are deliberately not exposed.1 endpoint(s)45Configure the self-registration policySpans both profiles. Read and set the single global policy that governs which sign-up methods the public profile offers for every tenant. The write lives here on the control plane; the public profile only reads it. Requires iam-platform-admin.3 endpoint(s)46Configure the login policySpans both profiles. The sign-in counterpart of scenario 45: a single GLOBAL setting governing which methods an EXISTING account may authenticate with, for every tenant. The write lives here on the control plane; the public profile only reads it. Requires iam-platform-admin. Restricting sign-up and restricting sign-in are separate levers on purpose — one must not quietly move the other.3 endpoint(s)47Register an app and enable it for a tenantStanding up a new product app is TWO separate acts, deliberately: registering the app (platform configuration, one row serving every tenant) and enabling a tenant for it (access control, per tenant). Neither grants any individual anything — that is scenario 56.6 endpoint(s)MULTI_APP_ENABLED48Diagnose an app that is missing a memberA tenant admin reports that someone who plainly exists in IAM is absent from the product app — no row, or a stale one. The app holds its OWN copy of identity, maintained by the asynchronous projection, so “missing downstream” and “missing in IAM” are different faults with different answers. This is how an operator tells them apart without guessing.3 endpoint(s)MULTI_APP_ENABLED49Internal discovery & health (ops)Not user flows — these serve the operator-profile infrastructure: internal liveness, the machine-generated API contract, and interactive exploration. Raw bodies, internal-only ingress.3 endpoint(s)
50Inviting a teammate by emailAn admin adds someone to their tenant by email address; the invitee finishes sign-up themselves and lands ALREADY SIGNED IN. This is R4 (a tenant admin inviting) and R5 (the founding admin of an operator-provisioned tenant, scenario 41).4 endpoint(s)51Inviting a teammate by phone (WhatsApp)The same flow as scenario 50, addressed by PHONE NUMBER and delivered over WhatsApp — for teammates who have no work email. This is R3. Routed by which identifier the admin supplies; the two are mutually exclusive.3 endpoint(s)52Accepting an invitation when you already have an accountOne identifier is one identity across every tenant, so POST /v1/invitations REUSES an existing account rather than duplicating it. The invitee therefore arrives at the accept screen in one of two entirely different situations, and the screen has to know which BEFORE it renders — which is what POST /v1/auth/invitations/describe is for.4 endpoint(s)53Accepting an invitation while signed inA user who already holds a session finds an invitation in their own membership list and accepts it in place — no link, no code. The signed-in alternative to scenario 52, which starts from the invitation message; which one applies is decided by whether the invitee holds a session.3 endpoint(s)54Resending an invitation that never arrivedThe invitee reports “I never got it” — or got only the 15-minute code and let it lapse. The admin re-delivers THE SAME pending invitation with fresh secrets; nothing about the membership changes. Step 1½ of scenario 50 / 51, not a new invite: POST /v1/invitations answers 409 while an open invitation exists — even a lapsed one — so this is the only path that helps during the link's 7 days.2 endpoint(s)55Administering members and rolesThe admin-only surface for managing the caller's tenant. The tenant is always the admin's own (principal.tenantId).9 endpoint(s)56Managing which apps a member may useThe tenant-admin half of the app dimension, and the sibling of scenario 55: roles say what a member may do INSIDE an app, grants say which apps they may enter at all. An operator reaches this the same way — through act-as (scenario 43). The tenant must first be enabled for the app (scenario 47). THIS IS THE ADMIN-DRIVEN HALF: a member can also acquire a FREE-TO-ACQUIRE app for an organization they administer themselves, without an admin or an operator, at scenario 57 — the same write, reached from the other side.4 endpoint(s)MULTI_APP_ENABLED57Joining an app you already have an account forThe mirror of scenario 1 for someone who is ALREADY REGISTERED. An app the operator has marked is_auto_enabled_on_register (scenario 47) is free to acquire at sign-up — a visitor with no account gets a tenant, an enablement and a grant in one transaction just by filling in the registration page. A user who already has an account, opening that same page, is refused 409 …_already_registered, and had no other route: every enablement write was an operator endpoint or that one-shot inside registration. The rule this closes: A CALLER MAY ACQUIRE THE APP EXACTLY WHEN AN ANONYMOUS VISITOR COULD, so both calls gate on the same registry predicates the registration hand-off uses, refusals included. This is NOT the paid subscription path — an app the operator has not opted in is refused here, and reaching it means subscribing, not joining.5 endpoint(s)58Reading the tenant audit logAn admin reviews tenant security events; filterable + cursor-paginated.1 endpoint(s)