Decommission 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.
- PATCH /v1/admin/tenants/{tenantId} { action: suspend } → frozen (step-up)
- PATCH /v1/admin/tenants/{tenantId} { action: restore } → back to active (NO step-up)
- PATCH /v1/admin/tenants/{tenantId} { action: soft-delete } → soft-deleted (step-up)
- DELETE /v1/admin/tenants/{tenantId} → hard delete (four-eyes + step-up)
/v1/admin/tenants/{tenantId}adminSuspend tenant (step-up)Lifecycle branch: exactly one of { name } or { action } — neither or both is a 400, and stepUp on a rename is refused rather than ignored. suspend → frozen. Step-up required. A frozen tenant refuses new tokens: its members' logins and switch-tenant calls answer 403 tenant_unavailable (scenarios 9 / 20). THE OTHER BRANCH OF THIS ENDPOINT IS A RENAME — send { name } alone, with no stepUp. It has no scenario of its own because it is not a lifecycle act, but it is worth knowing that it EMITS NO PROJECTION EVENT: the tenant's ledger row already exists, so the reconciler re-emits the new name within PROJECTION_RECONCILE_INTERVAL_MS (default 1h) and an app's copy is stale until then BY DESIGN. That is a stale row, not a missing one, and a resync does not shorten it (scenario 42).
Field guide — what each value means & where it comes from6 fields
Updates a tenant with EXACTLY ONE of a rename ({ name }) or a lifecycle transition ({ action, stepUp? }); the body is .strict() so unknown fields fail. Auth needs the gateway context (handled) plus the tenant-lifecycle-operator role; suspend/soft-delete also require a step-up factor (stepUp), while restore does not and rename must not carry stepUp.
tenantIdpathUUIDrequiredfrom a responsenamebodystringconditionalyou chooseactionbodyenumconditionalyou choosesuspend— Transitions the tenant to status 'frozen'; requires a step-up factor; audits tenant_suspended and emits the matching outbox event.restore— Transitions the tenant back to status 'active'; no step-up required; audits tenant_restored and emits the matching outbox event.soft-delete— Marks the tenant deleted; requires a step-up factor; audits tenant_deleted and emits the tenant.deleted outbox event.
stepUpbodyobjectconditionalyou choosestepUp.passwordbodystringconditionalyou choosestepUp.recoveryCodebodystringconditionalauthenticatorPATCH https://api.kerja.team/v1/admin/tenants/1c2d0000-0000-7000-8000-0000000000acResponse guide — what comes back & what each value means7 fields
200 OK returning the updated Tenant wrapped in the { data } envelope (no token/cookie or Cache-Control headers); status reflects the applied transition (suspend -> frozen, restore -> active, soft-delete marks the tenant deleted).
dataobjectalwaysdata.idUUIDalwaysdata.namestringalwaysdata.slugstringalwaysdata.statusenumalwaysprovisioning— Tenant is being set up and not yet usable.active— Tenant is live and operational; result of a restore.frozen— Tenant is suspended; result of a suspend action.archived— Tenant has been archived/retired.
data.createdAtISO-8601alwaysdata.updatedAtISO-8601always/v1/admin/tenants/{tenantId}adminRestore tenant (no step-up)Sends a frozen tenant back to active — the counterpart of suspend, and the ONLY lifecycle action that needs NO step-up (undoing a freeze is the safe direction).
Field guide — what each value means & where it comes from6 fields
Updates a tenant with EXACTLY ONE of a rename ({ name }) or a lifecycle transition ({ action, stepUp? }); the body is .strict() so unknown fields fail. Auth needs the gateway context (handled) plus the tenant-lifecycle-operator role; suspend/soft-delete also require a step-up factor (stepUp), while restore does not and rename must not carry stepUp.
tenantIdpathUUIDrequiredfrom a responsenamebodystringconditionalyou chooseactionbodyenumconditionalyou choosesuspend— Transitions the tenant to status 'frozen'; requires a step-up factor; audits tenant_suspended and emits the matching outbox event.restore— Transitions the tenant back to status 'active'; no step-up required; audits tenant_restored and emits the matching outbox event.soft-delete— Marks the tenant deleted; requires a step-up factor; audits tenant_deleted and emits the tenant.deleted outbox event.
stepUpbodyobjectconditionalyou choosestepUp.passwordbodystringconditionalyou choosestepUp.recoveryCodebodystringconditionalauthenticatorPATCH https://api.kerja.team/v1/admin/tenants/1c2d0000-0000-7000-8000-0000000000acResponse guide — what comes back & what each value means7 fields
200 OK returning the updated Tenant wrapped in the { data } envelope (no token/cookie or Cache-Control headers); status reflects the applied transition (suspend -> frozen, restore -> active, soft-delete marks the tenant deleted).
dataobjectalwaysdata.idUUIDalwaysdata.namestringalwaysdata.slugstringalwaysdata.statusenumalwaysprovisioning— Tenant is being set up and not yet usable.active— Tenant is live and operational; result of a restore.frozen— Tenant is suspended; result of a suspend action.archived— Tenant has been archived/retired.
data.createdAtISO-8601alwaysdata.updatedAtISO-8601always/v1/admin/tenants/{tenantId}adminSoft-delete tenant (step-up)Marks the tenant soft-deleted — the prerequisite for a hard delete. Step-up required.
Field guide — what each value means & where it comes from6 fields
Updates a tenant with EXACTLY ONE of a rename ({ name }) or a lifecycle transition ({ action, stepUp? }); the body is .strict() so unknown fields fail. Auth needs the gateway context (handled) plus the tenant-lifecycle-operator role; suspend/soft-delete also require a step-up factor (stepUp), while restore does not and rename must not carry stepUp.
tenantIdpathUUIDrequiredfrom a responsenamebodystringconditionalyou chooseactionbodyenumconditionalyou choosesuspend— Transitions the tenant to status 'frozen'; requires a step-up factor; audits tenant_suspended and emits the matching outbox event.restore— Transitions the tenant back to status 'active'; no step-up required; audits tenant_restored and emits the matching outbox event.soft-delete— Marks the tenant deleted; requires a step-up factor; audits tenant_deleted and emits the tenant.deleted outbox event.
stepUpbodyobjectconditionalyou choosestepUp.passwordbodystringconditionalyou choosestepUp.recoveryCodebodystringconditionalauthenticatorPATCH https://api.kerja.team/v1/admin/tenants/1c2d0000-0000-7000-8000-0000000000acResponse guide — what comes back & what each value means7 fields
200 OK returning the updated Tenant wrapped in the { data } envelope (no token/cookie or Cache-Control headers); status reflects the applied transition (suspend -> frozen, restore -> active, soft-delete marks the tenant deleted).
dataobjectalwaysdata.idUUIDalwaysdata.namestringalwaysdata.slugstringalwaysdata.statusenumalwaysprovisioning— Tenant is being set up and not yet usable.active— Tenant is live and operational; result of a restore.frozen— Tenant is suspended; result of a suspend action.archived— Tenant has been archived/retired.
data.createdAtISO-8601alwaysdata.updatedAtISO-8601always/v1/admin/tenants/{tenantId}adminHard-delete tenant (four-eyes + step-up)Phase two. The tenant must already be soft-deleted (else 422). approverOperatorId must differ from the caller and hold tenant-lifecycle-operator (else 403, four-eyes). Produces a deletion attestation.
Field guide — what each value means & where it comes from5 fields
Hard-deletes an already soft-deleted tenant; phase two of the lifecycle. Requires the tenant-lifecycle-operator role plus step-up re-auth and four-eyes (a second operator approver who differs from you). No Idempotency-Key needed. Authorization / x-gateway-context are handled by the tester and gateway. Body is .strict() (unknown fields rejected).
tenantIdpathUUIDrequiredfrom a responsestepUp.passwordbodystringconditionalyou choosestepUp.recoveryCodebodystringconditionalauthenticatorapproverOperatorIdbodyUUIDrequiredfrom a responsereasonbodystringrequiredyou chooseDELETE https://api.kerja.team/v1/admin/tenants/1c2d0000-0000-7000-8000-0000000000acResponse guide — what comes back & what each value means4 fields
200 OK with a { data } envelope wrapping a TenantDeletionAttestation receipt; no special response headers (no Set-Cookie, no Cache-Control: no-store). The body confirms the hard-delete was attested under four-eyes approval.
data.tenantIdUUIDalwaysdata.attestedByUUIDalwaysdata.approverUUIDalwaysdata.attestedAtISO-8601always