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.
Every organization gets an ID like org_0kfz3m8q1w5e9r2t6y4u7i3o5 — the org_ prefix followed by 25 lowercase alphanumeric characters encoding a UUIDv7.
You can rely on:
org_... reference in your database can go stale, but it can never silently start pointing at a different tenant.created_at when ordering matters.An organization is always in exactly one state:
| Status | Meaning | Reversible? |
|---|---|---|
active | Normal operation | — |
suspended | Temporarily disabled: excluded from new tokens, invitations frozen | Yes — set back to active |
deleting | Transitional, set internally while the delete cascade runs | No |
| (deleted) | Gone. Requests return 404 | No |
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 is enforced at token issuance. The moment an organization is suspended, it is excluded from every newly issued token:
organizations claim in new access and ID tokens omits the suspended organization./userinfo — responses stop including the suspended organization immediately.What suspension does not do:
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.
Real status transitions emit dedicated events in addition to the generic organization.updated:
organization.suspended — fired on active → suspendedorganization.reactivated — fired on suspended → activeBoth 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.
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:
api-key.deleted audit event. If any revocation fails, the deletion aborts (the organization stays in deleting) and the delete can simply be retried.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:
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 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.
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 this | Reflected in tokens |
|---|---|
| Add a member | Next issuance (login / refresh / userinfo) |
| Remove a member | Next issuance — org claims disappear |
| Change membership scopes or groups | Next issuance — claims carry the new effective scopes |
| Suspend a membership | Next issuance — org claims disappear |
| Suspend the organization | Next issuance, for every member |
| Delete the organization | Next 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.
If a ≤30-minute window on already-issued tokens is too wide for an operation:
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.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.
PATCH the organization with status: "suspended", a status_reason naming the invoice, and status_by: "billing-service".organization.suspended → mark the tenant restricted in your own database and invalidate any cached sessions for it.PATCH back to status: "active". organization.reactivated fires; pending invitations become acceptable again; the next refresh restores claims.404.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.organizations claim is structuredIf-Match for safe automation