Provision a tenant
Review 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.
- GET /v1/admin/tenants → review existing tenants (status filter, cursor-paginated)
- POST /v1/admin/tenants → 201 (tenant + founding-admin membership; single-use invitation — 7-day link AND a 6-digit code — out-of-band; meta.invitationSent = true)
- POST /v1/auth/invitations/accept (founding admin, PUBLIC profile) → sets their password with either half and is signed in immediately
- GET /v1/admin/tenants/{tenantId} → inspect the tenant and its member count once the admin has accepted
/v1/admin/tenantsadminList tenantsReview existing tenants before provisioning a new one.
Field guide — what each value means & where it comes from3 fields
No request body. Lists tenants (cursor-paginated) and requires a gateway context whose principal holds the tenant-lifecycle-operator role (Authorization / x-gateway-context are handled by the tester + gateway). All three query params are optional.
limitqueryintegeroptionalyou choosecursorquerystringoptionalfrom a responsestatusqueryenumoptionalyou chooseprovisioning— Returns only tenants still being provisioned (not yet fully active).active— Returns only active, fully operational tenants.frozen— Returns only frozen tenants (suspended access).archived— Returns only archived tenants.
GET https://api.kerja.team/v1/admin/tenants?status=active&limit=20Response guide — what comes back & what each value means9 fields
200 OK with the standard { data, meta } envelope (no special response headers — no Set-Cookie, no Cache-Control). `data` is an ARRAY of tenant summary objects; `meta` carries the cursor-pagination info (count and nextCursor).
data[]arrayalwaysdata[].idUUIDalwaysdata[].namestringalwaysdata[].slugstringalwaysdata[].statusenumalwaysprovisioning— Tenant is being set up; not yet fully active.active— Tenant is live and fully usable.frozen— Tenant access is suspended/locked but data retained.archived— Tenant is decommissioned/retired.
data[].createdAtISO-8601alwaysdata[].updatedAtISO-8601alwaysmeta.countintegeralwaysmeta.nextCursorstringalwaysnull— No further pages — this is the last page of results.
/v1/admin/tenantsadminProvision tenant (admin)Needs Idempotency-Key: <uuid>. slug is an optional lowercase DNS label. Creates the tenant (status active) and its founding-admin membership; a single-use invitation carrying a 7-day link AND a 6-digit code is delivered out-of-band, and meta.invitationSent = true. The control plane stores an unusable placeholder hash and NEVER learns the founding admin's password — they set it themselves on the public profile below. Same-key + same-body retry replays the 201.
Field guide — what each value means & where it comes from4 fields
Provisions a tenant and invites its founding admin in one transaction; requires the tenant-lifecycle-operator role (Authorization / x-gateway-context handled by the tester+gateway) and a fresh Idempotency-Key header per request. The invite token is delivered out-of-band (email) and never returned.
Idempotency-KeyheaderUUIDrequiredclient-generatednamebodystringrequiredyou chooseslugbodystringoptionalyou choosefoundingAdminEmailbodystringrequiredyou choosePOST https://api.kerja.team/v1/admin/tenantsResponse guide — what comes back & what each value means7 fields
201 Created. Body is the standard { data } envelope (plus a meta object); data is the newly provisioned Tenant object. No notable response headers (no Set-Cookie / Cache-Control); the invite token is delivered out-of-band and never returned.
data.idUUIDalwaysdata.namestringalwaysdata.slugstringalwaysdata.statusenumalwaysprovisioning— Tenant record is being set up; not yet generally usable.active— Tenant is live and usable; the value returned by this endpoint on success.frozen— Tenant is suspended; access blocked pending restore.archived— Tenant is soft-deleted/retired.
data.createdAtISO-8601alwaysdata.updatedAtISO-8601alwaysmeta.invitationSentbooleanalwaystrue— Founding-admin invitation was sent; the admin sets their password via the public profile flow (§3.15).
/v1/auth/invitations/acceptpublicFounding admin accepts (public profile)Cross-profile hop: this call runs on the PUBLIC surface, not the control plane. Submit either { token, password } (the 7-day link) or { email, code, password } (the 6-digit code). Since Enhancement 4 acceptance RETURNS AN ACCESS TOKEN (auto-captured), so the founding admin does not have to log in separately afterwards — see scenario 44.
Field guide — what each value means & where it comes from7 fields
Public endpoint (no Authorization needed) that finalizes a tenant invitation by setting the invited user's password and activating their membership — and, since Enhancement 4, ENDS IN A SESSION, so the invitee is not asked to log in with a password they created seconds earlier. The message carries a 7-day link AND a 6-digit code; submit either. Works for an email invite (R4/R5, amr [pwd, email]) and a WhatsApp phone invite (R3, amr [pwd, sms] — which also sets phone_verified_at and login_mode='phone').
tokenbodystringconditionalout-of-bandemailbodystringconditionalyou choosephonebodystringconditionalyou choosecodebodystringconditionalout-of-bandpasswordbodystringconditionalyou choosedisplayNamebodystringoptionalyou chooseclientIdbodystringoptionalyou choosePOST https://api.kerja.team/v1/auth/invitations/acceptResponse guide — what comes back & what each value means6 fields
200 OK with the { data } envelope. The 200 means the invitation (link OR short code — one row, either half) was valid and consumed, and the invited membership is now active. WHAT ELSE IT WROTE DEPENDS ON WHO THE INVITEE IS. For a NEW identity: the password is set, the channel marked verified, the status set to active (user.upserted enqueued, invitation_accepted audited). For an identity that ALREADY EXISTS, accept writes NOTHING global — not the password, not the status, not the login mode, not the display name — because the invitation is tenant-scoped while the credential is global; a globally disabled identity therefore stays disabled. POST /v1/auth/invitations/describe (scenario 46) is what tells the client which branch it is in, via needsPassword, and sending the wrong shape is a 422 either way. SINCE ENHANCEMENT 4 IT ENDS IN A SESSION, so the invitee is not asked to log in with a password they created seconds earlier. The membership is in the INVITER's tenant and the invitee gets no tenant of their own — a regression test pins this. Accepting also earns device trust on that browser, since the channel was just proven.
data.accessTokenstringalwaysdata.tokenTypestringalwaysdata.expiresInnumberalwaysdata.userobjectalwaysSet-Cookie: refresh_tokencookiealwaysSet-Cookie: device_tokencookieconditional/v1/admin/tenants/{tenantId}adminGet one tenantReturns the tenant plus its active member count (TenantDetail).
Field guide — what each value means & where it comes from1 field
No request body. A bodyless read returning one tenant plus its active member count; requires gateway auth (Authorization / x-gateway-context, handled by the tester) and the caller must hold the tenant-lifecycle-operator role. The only caller-supplied input is the tenantId path param.
tenantIdpathUUIDrequiredfrom a responseGET https://api.kerja.team/v1/admin/tenants/1c2d0000-0000-7000-8000-0000000000acResponse guide — what comes back & what each value means8 fields
200 OK with the standard { data } envelope; data is a single TenantDetail object (no pagination, no special response headers like Set-Cookie or Cache-Control).
dataobjectalwaysdata.idUUIDalwaysdata.namestringalwaysdata.slugstringalwaysdata.statusenumalwaysprovisioning— Tenant is being set up and is not yet fully active.active— Tenant is live and operational.frozen— Tenant is suspended (e.g. after a suspend transition); access is blocked until restored.archived— Tenant has been archived/soft-deleted and is no longer in active use.
data.createdAtISO-8601alwaysdata.updatedAtISO-8601alwaysdata.memberCountintegeralways