Guides

Organization lifecycle & guarantees

The operational contract for AuthPI organizations — ID stability, suspension semantics, hard-delete cascade, and exactly when membership changes reach tokens.

Last updated 2026-06-12

When you build billing, offboarding, or compliance flows on top of organizations, you need guarantees, not descriptions: what happens to issued tokens when you suspend an org? Does deletion cascade? Can an ID come back? This page is that contract. For what organizations are and how to model your tenants with them, see the Organizations concept.

Everything below describes verified production behavior, including the places where AuthPI deliberately does not do something (no forced logout on suspension, no automatic API-key revocation on delete). Those negative guarantees matter as much as the positive ones.

Organization IDs

Every organization gets an ID like org_0kfz3m8q1w5e9r2t6y4u7i3o5 — the org_ prefix followed by 25 lowercase alphanumeric characters encoding a UUIDv7.

You can rely on:

  • Server-generated. IDs are minted by AuthPI at creation. You cannot choose them, and they carry no meaning derived from the organization’s name.
  • Immutable. An organization’s ID never changes, no matter how often its name, status, or settings do. Safe to use as a foreign key in your own database.
  • Never reused. IDs are UUIDv7-based, so a deleted organization’s ID will not be assigned to a new organization — not in your account, not in anyone’s. A dangling org_... reference in your database can go stale, but it can never silently start pointing at a different tenant.
  • Time-ordered. UUIDv7 embeds a creation timestamp, so IDs sort roughly by creation order. Treat this as an implementation detail, not an API: use created_at when ordering matters.

The status model

An organization is always in exactly one state:

StatusMeaningReversible?
activeNormal operation
suspendedTemporarily disabled: excluded from new tokens, invitations frozenYes — set back to active
deletingTransitional, set internally while the delete cascade runsNo
(deleted)Gone. Requests return 404No

You move between active and suspended with a normal update:

curl -X PATCH "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/issuers/i_4r8w2k9m5x1p7q3e6t0y2u4i8/organizations/org_0kfz3m8q1w5e9r2t6y4u7i3o5" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "suspended",
    "status_reason": "Invoice 2026-0142 overdue 30 days",
    "status_by": "billing-service"
  }'

status_reason (up to 1,000 characters) and status_by (up to 200) are audit fields: they’re stored on the organization, returned on reads, and included in the lifecycle events below. Set them every time — three months later, “why is this org suspended?” should be answerable from the record itself.

Suspension: what it does and doesn’t do

Suspension is enforced at token issuance. The moment an organization is suspended, it is excluded from every newly issued token:

  • Fresh logins — the organizations claim in new access and ID tokens omits the suspended organization.
  • Token refreshes — memberships are re-read on every refresh, so the next refresh drops the organization’s claims.
  • /userinfo — responses stop including the suspended organization immediately.

What suspension does not do:

  • It does not log anyone out. Sessions and refresh tokens remain valid. Users keep working in their other organizations without interruption — suspension removes one organization from their tokens, not the user from your application.
  • It does not revoke already-issued access tokens. Tokens issued before the suspension keep their claims until they expire. The staleness window is therefore bounded by your access token lifetime — 30 minutes by default, configurable per client. If your threat model needs a tighter bound, shorten the access token TTL on the relevant client; see Tightening the propagation bound.
  • It does not freeze administration. The Core API keeps working against a suspended organization: you can read it, update it, manage members, and even add members while preparing for reactivation. The one user-facing flow that is blocked is invitation acceptance — pending invitations stay pending and become acceptable again after reactivation.

Suspending a single membership

Sometimes the tenant is fine and one member is the problem. Memberships have their own active/suspended status, enforced by the same issuance-time check: a suspended membership drops that organization from that one user’s tokens, while every other member is untouched.

curl -X PATCH "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/issuers/i_4r8w2k9m5x1p7q3e6t0y2u4i8/organizations/org_0kfz3m8q1w5e9r2t6y4u7i3o5/members/usr_8t2y6u4i0o5p3a7s1d9f2g4h6" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "status": "suspended" }'

Prefer this over removal when the situation may be reversed — reactivating a membership restores the previous scopes and groups exactly, whereas re-adding a removed member starts from scratch.

Lifecycle events

Real status transitions emit dedicated events in addition to the generic organization.updated:

  • organization.suspended — fired on active → suspended
  • organization.reactivated — fired on suspended → active

Both carry status, previous_status, status_at, and — when provided on the update — status_reason and status_by. A PATCH that re-asserts the current status does not fire them, so you can treat each event as a genuine transition. Subscribe via webhooks to drive your own side effects, such as invalidating your application’s sessions for that tenant the moment suspension lands rather than waiting out the token TTL.

Deletion: permanent and immediate

Deleting an organization is a hard delete with no grace period. This is deliberately different from users and issuers, which get a soft-delete window — organizations are assumed to be deliberate, admin-initiated removals.

curl -X DELETE "https://api.authpi.com/v1/accounts/acc_7k2m9x4p1q8w5e3r6t0y2u4i7/issuers/i_4r8w2k9m5x1p7q3e6t0y2u4i8/organizations/org_0kfz3m8q1w5e9r2t6y4u7i3o5" \
  -u "$AUTHPI_KEY_ID:$AUTHPI_KEY_SECRET" \
  -H "If-Match: \"1749722400000\""

A successful delete returns 204. The optional If-Match header makes the delete conditional on the organization’s current ETag — worth using in automation so a concurrent change fails the delete instead of being silently destroyed. See Conditional requests.

During the cascade the organization passes through the internal deleting status, which blocks new memberships and new API keys from racing in. The cascade removes:

  • All organization API keys — revoked before anything else is torn down, so a key can never outlive its organization. Each revocation emits an api-key.deleted audit event. If any revocation fails, the deletion aborts (the organization stays in deleting) and the delete can simply be retried.
  • All memberships — members lose the organization, but the user accounts themselves are untouched and keep their other memberships.
  • All groups defined in the organization.
  • All invitations, in every state. Pending invitation links are invalidated immediately.
  • SSO configuration and any SSO domain mappings.
  • The organization record itself — subsequent reads return 404.

Token claims follow the same bounded-staleness rule as suspension: already-issued access tokens keep the deleted organization’s claims until they expire (≤ the access token TTL); the next login, refresh, or /userinfo call reflects the deletion.

One thing deletion does not do:

  • It does not emit per-membership events. The cascade fires a single organization.deleted event — you will not receive an organization.membership.deleted event for each member. If your webhook consumer maintains a membership mirror, treat organization.deleted as “all memberships of this org are gone” rather than waiting for individual deletions that will never arrive.

Suspend or delete?

Suspend when the situation might be resolved — non-payment, a policy review, a paused contract. The organization’s data, memberships, settings, and invitations all survive suspension intact, and reactivation is a one-field update.

Delete only when you’re certain: deletion is immediate, the cascade is irreversible, and there is no recovery window. A common offboarding sequence is suspend now, delete after the retention period — suspension stops access today while your compliance clock runs.

The membership → token propagation contract

Organization claims in tokens are computed fresh at every issuance. AuthPI does not copy memberships into long-lived session state — each login, token refresh, and /userinfo call re-reads the membership index. That single design fact generates the whole propagation contract:

You do thisReflected in tokens
Add a memberNext issuance (login / refresh / userinfo)
Remove a memberNext issuance — org claims disappear
Change membership scopes or groupsNext issuance — claims carry the new effective scopes
Suspend a membershipNext issuance — org claims disappear
Suspend the organizationNext issuance, for every member
Delete the organizationNext issuance, for every member

In every row, “next issuance” means the change is live for new tokens immediately — the only lag is tokens that already exist. The worst-case staleness is the remaining lifetime of the oldest valid access token, which is bounded by the access token TTL (default 30 minutes).

One consequence to internalize: removing a member does not revoke their refresh token. A removed member keeps a valid session and keeps refreshing — they just receive tokens without that organization’s claims from the next refresh onward. They lose the tenant, not their account. This is intentional: the user may have other memberships, and their relationship with your application is independent of any one organization. If the user is the problem, act on the user (block them or revoke their sessions), not on the membership.

Tightening the propagation bound

If a ≤30-minute window on already-issued tokens is too wide for an operation:

  1. Shorten the access token TTL on the relevant client. The staleness bound is exactly the access token lifetime, so a client configured with a 5-minute TTL has a 5-minute worst case. Trade-off: more refresh traffic.
  2. React to events instead of waiting for expiry. Subscribe to organization.suspended, organization.membership.deleted, and organization.membership.updated webhook events, and have your application invalidate its own sessions or caches for the affected tenant when they fire. This closes the gap to webhook-delivery latency — typically seconds.
  3. Re-check authorization server-side for destructive operations. For genuinely sensitive actions (payouts, data export, member management), don’t trust a 25-minute-old token’s organizations claim — list the organization’s members (GET .../organizations/{org_id}/members, filterable by status) and verify the user still appears as an active member at that moment.

Most applications need only #1 or #2. The pattern to avoid is rebuilding your own membership store from tokens and treating it as durable — the token is a bounded-staleness cache of memberships, and the API is the source of truth.

Recipes

Billing-driven suspension

  1. Your billing system flags the tenant → PATCH the organization with status: "suspended", a status_reason naming the invoice, and status_by: "billing-service".
  2. Your webhook consumer receives organization.suspended → mark the tenant restricted in your own database and invalidate any cached sessions for it.
  3. Within the access token TTL, every member’s tokens stop carrying the organization. Users can still log in and see their other tenants.
  4. Payment clears → PATCH back to status: "active". organization.reactivated fires; pending invitations become acceptable again; the next refresh restores claims.

Offboarding a customer

  1. Suspend first — access stops now, data survives.
  2. Export what compliance requires — members list, invitations, audit events. After deletion the API returns 404.
  3. Delete. The organization’s API keys are revoked automatically before anything else is removed (an api-key.deleted event fires for each). Verify the single organization.deleted event arrived, and clean up the tenant’s rows in your own database keyed by the (never-reused) org_id.

Next steps