Provisioning a New Tenant (ScaiKey → ScaiGrid)
Audience: ScaiLabs partners (resellers, MSPs, ISPs) onboarding a new end customer. Outcome: A new tenant in ScaiKey with a first Tenant Admin, the matching tenant in ScaiGrid with a Tenant Admin, and an API key that can reach only an approved set of LLM models.
1. Conventions#
| Variable | Meaning | Example |
|---|---|---|
$SCAIKEY |
ScaiKey base URL | https://scaikey.scailabs.ai |
$SCAIGRID |
ScaiGrid base URL | https://scaigrid.scailabs.ai |
$PARTNER_ID |
Your partner ID (ScaiKey prefix prt_) |
prt_... |
$SK_TOKEN |
ScaiKey admin bearer JWT | see §2 |
$SG_TOKEN |
ScaiGrid partner-admin bearer JWT | see §2 |
$TENANT_NAME / $TENANT_SLUG |
Customer display name / slug | Acme Corp / acme-corp |
$ADMIN_EMAIL |
First Tenant Admin's email | alice@acme.example |
ID prefixes differ per system — do not mix them up:
- ScaiKey: partners
prt_, tenantstnt_, usersusr_, groupsgrp_, appsapp_ - ScaiGrid: tenants are referenced in the docs as
tenant_acme, users asuser_abc, keys asapikey_xyz, API key secrets assgk_...
2. Prerequisites#
Before you start, confirm you hold all of the following.
2.1 ScaiKey credentials#
ScaiKey's admin API lives at /api/v1/admin/... and takes a Bearer JWT that either carries an admin role (super_admin, partner_admin, tenant_admin) or is a client_credentials platform token from a GLOBAL SERVICE application whose allowed_scopes include admin:read and admin:write.
For automated provisioning, use the service-application route:
- Have ScaiLabs register a
GLOBAL/SERVICEapplication for your partner organisation. - Ensure its
allowed_scopescontainsadmin:readandadmin:write. Scopes are constrained by the application's registeredallowed_scopes— asking for a scope the app isn't registered for silently narrows the token; it does not error. - Obtain a token via
client_credentials. The resulting JWT carriesplatform_token: trueandapp_scope: GLOBAL, which is what unlocks cross-tenant admin access.
If you receive 403 Platform token requires admin:read or admin:write scope, fix the application's allowed_scopes — not the token request.
2.2 ScaiGrid credentials#
Recommended: a service principal via client_credentials. ScaiGrid accepts OAuth 2.0 client_credentials access tokens issued by ScaiKey — no interactive PKCE flow and no API key needed. Present the JWT as Authorization: Bearer <jwt>, exactly like a user token.
- The token's
subis theclient_id, and it carriestoken_type=client_credentials. - ScaiGrid resolves it to a service identity and honours the roles and module permissions assigned to that
client_id. With nothing assigned it falls back totenant_user— so assignpartner_adminexplicitly, or your provisioning calls will fail with403in ways that look like a token problem. - Registering the client and running the grant are ScaiKey operations; ScaiGrid only consumes the token.
This is the supported path for headless partner automation, and it means the whole runbook can run unattended.
Alternatively, a human partner admin can obtain a JWT through the ScaiKey OAuth 2.0 + PKCE flow (/v1/auth/identify → /v1/auth/authorize → SSO at ScaiKey → /v1/auth/token).
partner_admin in ScaiGrid grants: models:list, accounting:view_own, accounting:view_tenant, accounting:view_partner, accounting:manage_budgets, users:manage, admin:access.
2.3 Capacity check#
Your partner record carries max_tenants, max_users_per_tenant and max_applications_per_tenant as top-level integer fields. Check headroom before provisioning:
1 2 | |
A partner_admin can read its own partner record, including the max_tenants quota and settings. It cannot read other partners, and cannot create or delete partners.
2.4 Confirm tenant self-management is enabled#
Do this before anything else — it determines whether Phase 1 is something you can run at all.
By default, creating, updating and deleting tenants is a super-admin action. A super-admin can delegate it per partner with the tenant self-management flag. With it on, your partner_admins may create tenants under your own partner, up to max_tenants. With it off, tenant writes return 403 "Tenant self-management is not enabled for this partner" and provisioning becomes a ticket to ScaiLabs.
Check settings.allow_tenant_self_management on the partner record you fetched in §2.3. If it's absent or false, request it from ScaiLabs — only a super-admin can set it:
1 2 | |
A partner admin cannot enable this for their own partner, so don't try.
2.5 Information to collect from the customer#
- Legal / display name and preferred slug
- Contact email for the tenant
- First Tenant Admin: email, first name, last name
- MFA requirement, allowed MFA methods, password policy, session timeout
- The exact list of model slugs the customer is entitled to (e.g.
scailabs/poolnoodle-omni,mistral/large)
3. Phase 1 — Create the tenant in ScaiKey#
Step 1.1 — Create the tenant#
1 2 3 4 5 6 7 8 9 10 | |
Expected: 201 Created with the new tenant object. Record the tnt_... ID as $SK_TENANT_ID.
Fields. Required: name, slug — plus partner_id for super-admin callers. Optional: contact_email, status, settings, branding. partner_id is accepted on create, so there's no separate step to attach the tenant to your partner. Full field table on the Tenants API reference.
Who may create a tenant. A partner_admin can, provided tenant self-management is enabled for the partner (§2.4). For a partner admin, partner_id is ignored and pinned server-side to your own partner — you cannot create a tenant under another one even by passing a different value, so the field in the payload above is belt-and-braces. Creation is capped at the partner's max_tenants.
Step 1.2 — Apply tenant security settings#
1 2 3 4 5 6 7 8 9 10 | |
Documented settings keys: mfa_required (bool), mfa_methods (list from totp, sms, email, webauthn), password_policy (dict), session_timeout (minutes).
The internal structure of password_policy is not currently specified; confirm the shape you need with ScaiLabs support before relying on it.
Step 1.3 — Verify#
1 2 | |
Check status, slug, and partner_id.
4. Phase 2 — Create the first Tenant Admin in ScaiKey#
This is two operations: create the user in the tenant directory, then grant the admin role.
Step 2.1 — Create the user#
Use the tenant-scoped endpoint — it has a fully documented UserCreate schema, unlike the /admin/users variant:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
UserCreate fields: email (required), username, first_name, last_name, display_name, phone, locale (default en), timezone (default UTC), password, ou_id, group_ids, custom_attributes, require_password_change (default false), send_welcome_email (default true).
Expected: 201 Created. Record the user ID as $SK_USER_ID.
Notes:
- Omit
passwordand leavesend_welcome_email: trueso the customer sets their own credential. If you must set one, userequire_password_change: true. - The user's
statuswill be one ofACTIVE,PENDING_EMAIL_VERIFICATION,PENDING_APPROVAL,DISABLED,LOCKED. A new user with a welcome email typically lands inPENDING_EMAIL_VERIFICATION. identity_sourcewill beLOCALhere. It becomesOIDC/SAML/SCIM/LDAPonly if the user is federated. If you later attach a customer IdP, runGET /api/v1/admin/users/identity-source/mismatches?fix=trueto reconcile.
Step 2.2 — Grant the TENANT_ADMIN role#
1 2 3 4 5 6 7 8 | |
Documented fields: user_id, role (SUPER_ADMIN | PARTNER_ADMIN | TENANT_ADMIN), plus tenant_id (required for TENANT_ADMIN), partner_id (required for PARTNER_ADMIN), and optional expires_at (ISO 8601).
Expected: 201 Created. Record the role_id — you need it to revoke later.
A partner_admin can do this. Assigning and revoking TENANT_ADMIN for tenants under its own partner is exactly how a partner seats the first Tenant Admin of a tenant it provisioned. A partner admin cannot assign SUPER_ADMIN or PARTNER_ADMIN, cannot touch another partner, and cannot edit an existing assignment — PATCH /{id} is super-admin-only.
Because PATCH is closed to you, to change an assignment, DELETE it and POST a new one. Mind the cleanup_orphaned_user default on delete (see §8) — it can soft-delete the user out from under you if they hold no other admin role and have never logged in. Pass ?cleanup_orphaned_user=false when you intend to re-grant.
Note this step does not depend on the tenant self-management flag. That flag gates tenant writes only; seating a Tenant Admin is always available to a partner admin within its own partner.
Reads are scoped too: GET on the list, on a user, or on a single role_id returns only assignments within your own partner. An out-of-scope role_id returns 404, not 403 — so a role that exists but belongs to another partner is indistinguishable from one that doesn't exist. Don't treat a 404 here as proof the assignment was deleted.
Step 2.3 — Assign the applications (this is what triggers provisioning downstream)#
This step does more than grant access — it's the provisioning trigger for every ScaiLabs app. Assigning at least one user in the tenant to an application causes that application to provision the tenant on its own side. This holds across the whole ScaiLabs portfolio, not just ScaiGrid.
The mechanism, precisely: ScaiKey notifies the target app; the app provisions the tenant itself. ScaiKey never reaches into the application's data. Notification goes one of two ways:
- Push — ScaiKey posts the user and
tenant_idto the application's registeredsync_webhook_url. - Pull — the application calls
GET /api/v1/admin/applications/{id}/effective-users, which returns the users along with full tenant slug, name and partner.
Two things follow from this that matter operationally. Provisioning depends on the downstream app being reachable and correctly configured, so a missing sync_webhook_url or a failing endpoint on the app's side looks identical to a missing assignment from ScaiKey's perspective. And because the app does the work, the tenant arrives with whatever defaults that app applies — ScaiKey doesn't dictate its shape.
So assigning your new Tenant Admin to the ScaiGrid application is what brings the tenant into existence in ScaiGrid. Do it here, in Phase 2, and Phase 3 becomes verification rather than creation.
See ScaiKey → Concepts → Applications for the full description of the sync contract.
Find the application first. The authoritative list for a tenant:
1 2 | |
Takes an admin token (super, partner or tenant admin), with results scoped to what the caller can see. Filters: tenant_id, partner_id, scope (GLOBAL / PARTNER / TENANT), application_type, search.
A tenant's usable set is its TENANT apps plus all GLOBAL apps. GLOBAL apps are cross-tenant, so filtering by tenant_id alone can look emptier than reality — check both scopes before concluding an app is missing.
Then assign the user, or a group they belong to, to each application the customer is entitled to. Group-based assignment is the better default: it keeps ScaiKey group membership as the single source of truth and pairs naturally with the ScaiGrid role mapping in Step 3.3.
Order matters. Assign the ScaiGrid application after the tenant settings in Step 1.2 and the admin role in Step 2.2, so the tenant materialises downstream with its final configuration and an admin already attached.
Timing. The sync starts as soon as the app has its first user — whether you assign that user directly or assign a group that already has members. It usually completes within seconds; allow up to 30 seconds before treating it as failed. Scripted provisioning should wait and poll Step 3.1 rather than proceeding straight to Phase 3, or the tenant lookup will race the sync and appear to fail.
Group assignment works — the trigger follows the membership. What starts the sync is a user reaching the app, not a direct assignment specifically, so assigning a populated group is equivalent. An empty group simply does nothing yet: assign the app first and the sync fires the moment you add the first user to that group.
So the order of "assign the app" and "add the user to the group" doesn't matter. What matters is where you put the wait — poll Step 3.1 after whichever of the two comes second, since that's the step that actually triggers the sync.
Verify the assignment took. To see what a specific user can actually use — direct assignments and group-derived ones, including GLOBAL apps:
1 2 | |
This is the check to run before polling for the tenant in Step 3.1 — if ScaiGrid isn't in this list, the sync has nothing to trigger on.
It's admin-gated and keyed by user_id, so it isn't a self-service call. Don't hand it to the customer's Tenant Admin as a "check your own access" endpoint; it requires assignment-admin rights.
Don't confuse it with
GET /api/v1/me/authorized-apps. That's a user-token call returning apps the current user has already connected to — an OAuth consent list, not a catalogue of what's available to them. It will not tell you whether an assignment succeeded, and an app can be assigned without appearing there until the user first authorises it.
Nothing is auto-assigned. No application is attached to a tenant or user automatically at creation — every one is an explicit assignment. Two consequences worth internalising:
- This step is never optional. A ScaiKey tenant with no app assignments exists in ScaiKey and nowhere else. Skip it and Phase 3 has nothing to find, no matter how long you wait.
- The app list is a complete inventory of a customer's reach. Since nothing arrives implicitly, what you see in
GET /api/v1/apps/users/{user_id}/applicationsis the whole picture — useful for entitlement reviews and offboarding, where an implicit default would otherwise be easy to miss.
Step 2.4 — Verify#
1 2 | |
Confirm one active TENANT_ADMIN entry scoped to $SK_TENANT_ID.
5. Phase 3 — Confirm the tenant and seat the Tenant Admin in ScaiGrid#
ScaiGrid maintains its own tenant hierarchy (partner → tenant → user), but you don't populate it directly. Tenants arrive from ScaiKey via app assignment (Step 2.3); users originate in ScaiKey and are seated here by role mapping. Humans sign in via SSO, services authenticate with API keys or a client_credentials service principal.
Step 3.1 — Confirm the tenant arrived (do not create it)#
The tenant was provisioned in ScaiGrid by ScaiGrid itself, in response to the app assignment in Step 2.3. It carries the same tnt_ ID as its ScaiKey counterpart, and the flow is one-way, ScaiKey → ScaiGrid. Your job here is to confirm it landed, not to create it:
1 | |
Find $SK_TENANT_ID in the list and carry it forward as $SG_TENANT_ID — they're the same value.
Do not call POST /v1/tenants in this flow. Creating a tenant that already synced produces a duplicate, and you'll have two records competing for the same customer. That endpoint exists for ScaiGrid-only tenants with no ScaiKey counterpart, which is not this runbook's scenario.
If the tenant isn't there, don't reach for POST — go back and check that Step 2.3 actually assigned the ScaiGrid application to a user in the tenant. A missing tenant downstream almost always means a missing app assignment upstream.
Give it 30 seconds. The sync starts when the app first has a user behind it — assigned directly, or arriving through a group you've mapped (Step 2.3) — and usually finishes within seconds. If the tenant isn't in the list on your first call, wait and retry — a simple poll:
1 2 3 4 5 | |
Only after 30 seconds with no result should you treat it as a failure — and then check Step 2.3, not POST /v1/tenants.
Step 3.2 — Apply a tenant template (optional but recommended)#
If you maintain a standard customer profile, apply it now — it enables modules, creates custom roles and sets budget defaults in one call:
1 2 | |
Templates are additive — they never remove anything the tenant already has.
Step 3.3 — Get the admin user into ScaiGrid#
Users are either provisioned manually by a tenant admin, or appear automatically the first time they authenticate via ScaiKey SSO — depending on your group-mapping configuration.
Recommended: group mapping (works before first login). Create the ScaiKey group in Phase 2 (e.g. acme-admins), add the admin to it, then map it:
1 2 3 4 5 6 7 8 9 | |
auto_revoke: true means removing the user from the ScaiKey group strips the role on their next authentication. This is the cleanest lifecycle: ScaiKey group membership becomes the single source of truth.
Alternative: direct role assignment (after first login). Have the admin sign in once so the record exists, find them, then set roles:
1 2 3 4 5 6 | |
Both require users:manage, which partner_admin has.
Identity-only provisioning is by design. ScaiGrid has no POST /v1/users and is not meant to. Users originate in ScaiKey and reach ScaiGrid through sync; you seat them with role mappings or PUT /v1/users/{id}/roles. The group-mapping route above is therefore the intended path, not a workaround, and it's what lets provisioning finish without the customer logging in first.
tenant_admin in ScaiGrid grants models:list, models:use, api_keys:manage, modules:use, routing:view, accounting:view_tenant, accounting:manage_budgets, users:manage, webhooks:manage, modules:manage, admin:access.
Note: tenant admins and partner admins receive every module permission by default through a short-circuit in has_module_permission. Platform-scope module routes are gated separately on role, but be aware the customer's admin is broadly privileged across enabled modules.
Step 3.4 — Verify#
Have the admin sign in to the ScaiGrid admin UI and call:
1 | |
Confirm tenant_id matches $SG_TENANT_ID and roles contains tenant_admin.
6. Phase 4 — Restrict models and issue the API key#
Read this section fully before executing. ScaiGrid does not bind model permissions to an API key. The key inherits the permissions of the user it belongs to, and model entitlement is enforced by tenant-scoped access policies. The recipe below composes those two mechanisms; §6.5 explains what it does and does not guarantee.
Step 4.1 — Confirm the model slugs#
1 | |
Slug conventions: openai/gpt-4o (platform, provider-supplied), scailabs/poolnoodle-omni (platform, ScaiLabs), partner/{partner_slug}/..., tenant/{tenant_slug}/.... Slugs containing / need %2F encoding in path parameters.
Without an explicit access entry, models are implicitly allowed. Entitlement is therefore a deny-by-exception system, not an allow-list — which matters for the ordering below.
Step 4.2 — Group the approved models#
1 2 3 4 5 6 7 8 | |
Record the group ID as $MODEL_GROUP_ID.
Step 4.3 — Grant the group, deny everything else#
Grant the entitled group:
1 2 3 4 5 6 7 8 9 10 | |
Then, because unlisted models are implicitly allowed, explicitly disable every model the customer is not entitled to:
1 2 3 4 5 6 7 8 9 | |
Repeat per excluded model, or maintain a second "denied" model group and disable it as a unit.
Entitlement resolution — now specified. Three rules govern this:
- Implicit allow. A model with no matching policy is allowed. Entitlement is deny-by-exception.
scope_typeistenantorpartner— those two values only. A partner-scope policy applies across all that partner's tenants, which is the efficient way to deny a model fleet-wide.- Most-restrictive-wins across partner + tenant. A model is usable only if it isn't disabled at either scope. Any matching
enabled: falsedenies. A model-slug deny overrides a model-group allow, at the same scope or across scopes.
So the ordering below is safe: the deny entries win regardless of what the group allow says.
Closed enrolment. Because a newly published platform model is visible until explicitly denied, for partners who need closed enrolment, run a deny-by-default reconciliation: deny new slugs, then allow the intended set per tenant or partner. Consider running this at partner scope so it covers every tenant you onboard, rather than repeating it per customer.
Step 4.4 — Create the API key#
The key belongs to one user in one tenant and inherits that user's permissions. Decide who owns it:
- Preferred: a dedicated service identity with the narrowest workable role (
tenant_user— which carriesmodels:use,api_keys:manage,modules:use,models:list,accounting:view_own), so the key cannot administer the tenant. Because ScaiGrid has no direct user-creation endpoint (§5.3), the service identity needs a ScaiKey user plus a group mapped totenant_user. - Avoid: issuing the integration key from the Tenant Admin's own account. That key would carry
users:manage,modules:manageandadmin:access, plus every module permission — far more than an inference integration needs.
Create it (called with the owning user's token):
1 2 3 4 5 6 7 8 9 | |
allowed_model_slugs is the per-key allowlist. Its semantics:
- A non-empty list narrows the key to exactly those slugs.
- It narrows only within what the tenant is already entitled to — it never widens access. Listing a model the tenant is denied does not grant it.
- Empty or absent means the key can use whatever the tenant can.
This is what lets two keys in the same tenant carry different model entitlements. It's a second, independent layer on top of the tenant/partner policies from §6.3 — so keep §6.3 in place as the contractual boundary and treat the allowlist as per-integration least privilege. Don't rely on the allowlist alone: it's per-key, so a new key created by the tenant admin defaults to the full tenant entitlement.
assigned_user_id is the key's permission source when set — the user whose permissions the key borrows. But the key's identity (its user_id, what /v1/me reports, and what audit and ownership checks key on) is always the creator/owner, never the assigned user. When assigned_user_id is null (as here), the key both is and draws its permissions from the owning service user — so make the creating token that dedicated service identity, not the tenant admin.
This owner-vs-assignee split is a footgun. A key scoped by assigned_user_id still acts as the owner for anything that checks the acting user, so a key minted by a tenant admin and pointed at a low-privilege user is not thereby a low-privilege key everywhere. Scope by who creates it, and treat assigned_user_id as a refinement rather than a containment boundary.
scopes in the response is informational, a readout rather than an input; don't try to set it.
Field behaviour.
allowed_model_slugs,assigned_user_id,nameandexpires_atare the fields on thePOST /v1/api-keyscreate body (APIKeyCreate). A non-emptyallowed_model_slugsis deduplicated and empty-to-nullnormalised server-side.assigned_user_idandassigned_group_idare mutually exclusive.scopesis response-only.
Response — the full key is shown exactly once; the database stores only a SHA-256 hash:
1 2 3 4 5 6 7 8 9 10 11 | |
Deliver the key to the customer through an out-of-band secure channel and record only apikey_xyz and its prefix in your own systems. Keys never expire unless you set expires_at — always set it.
Step 4.5 — What this actually enforces#
Be precise with the customer about the guarantee:
| Claim | True? |
|---|---|
| The key can only call models the tenant is entitled to | Yes — enforced at tenant and partner scope, most-restrictive-wins |
| The key can only call a subset of what other keys in the same tenant can call | Yes — via allowed_model_slugs on that key |
| The allowlist can grant a model the tenant is denied | No — it narrows only, never widens |
| Other keys in the tenant are also narrowed | No — the allowlist is per-key; a new key defaults to the full tenant entitlement |
| The key's blast radius is limited to one user's permissions | Yes — the owner's, or the assigned_user_id's when one is set; note the key still acts as the owner for identity and audit |
| The key can be revoked independently and immediately | Yes — DELETE /v1/api-keys/{key_id}, effective immediately including in-flight requests |
Two keys in one tenant with different model entitlements is now a supported configuration. The earlier workaround of splitting into two tenants is no longer needed.
The layering is worth stating plainly to customers: the tenant/partner policies are the contractual boundary, and the per-key allowlist is least privilege for one integration. A tenant admin holding api_keys:manage can mint a key with no allowlist and reach everything the tenant is entitled to — so the allowlist constrains the integration, not the customer.
Step 4.6 — Verify the key#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Do not skip the negative test. Given the implicit-allow default, it is the only thing that proves the entitlement actually bit.
Endpoint. The body above is OpenAI-shaped (
{model, messages}), so it goes to the OpenAI-compatible surface:POST $SCAIGRID/oai/v1/chat/completions, mounted at the/oai/v1prefix. There is no/v1/chat/completions— that path404s, and a404here is easy to mistake for the entitlement biting, which would make the negative test above pass for the wrong reason. ScaiGrid's native inference surface isPOST $SCAIGRID/v1/inference/chat, but it takes a different (non-messages) request shape; use the OpenAI path for the body shown.
7. Post-provisioning#
7.1 Budgets#
Set a spend cap so a runaway integration cannot generate an unbounded bill. Budgets enforce at partner, tenant and user level; a tenant over its cap blocks its users. See the Accounting and Budgets documentation.
7.2 Rate limits#
API keys, users, tenants and partners each carry independent limits. Exceeding one returns 429 RATE_LIMITED with Retry-After. The rate_limit field on a model-access policy (used in §6.3) applies at that policy's scope.
7.3 Handover pack for the customer#
- Tenant slug and ScaiGrid base URL
- Tenant Admin sign-in instructions (SSO via ScaiKey)
- API key, out of band, with its
expires_atand rotation date - The entitled model slugs
- Reminder: never embed
sgk_keys in client-side code; one key per service, per environment; rotate at minimum on personnel changes
7.4 Record in your own systems#
$SK_TENANT_ID, $SK_USER_ID, ScaiKey role_id, $SG_TENANT_ID, $MODEL_GROUP_ID, each model-access policy ID, apikey_xyz, and key expiry. You need all of them for §8.
8. Rollback and deprovisioning#
Run in this order — it revokes access before removing the records that let you find things.
- Revoke API keys —
DELETE $SCAIGRID/v1/api-keys/{key_id}. Immediate. - Suspend the ScaiGrid tenant —
PUT $SCAIGRID/v1/tenants/{id}with{"status": "suspended"}. Users get403 TENANT_SUSPENDED. Reversible with"active". - Revoke the ScaiKey admin role —
DELETE $SCAIKEY/api/v1/admin/admin-roles/{role_id}. Takes effect immediately for ScaiKey's own admin API (it does a live lookup). Notecleanup_orphaned_userdefaults totrueand will soft-delete the user if they have no remaining admin roles, exist in multiple tenants with the same email, and have never logged in. Pass?cleanup_orphaned_user=falseif you want the user preserved. - Disable or delete the ScaiKey user —
PUT $SCAIKEY/api/v1/tenants/{tenant_id}/users/{user_id}/statuswith{"status": "DISABLED", "terminate_sessions": true}, orDELETE(soft delete; requiresconfirm_emailas a query parameter). - Soft-delete the ScaiKey tenant —
DELETE $SCAIKEY/api/v1/admin/tenants/{tenant_id}.
Revocation timing — read before promising an SLA#
ScaiKey access tokens are stateless JWTs verified locally against the published JWKS. There is no per-token revocation list, so most "revoke" actions do not invalidate in-flight access tokens:
| Action | Effective |
|---|---|
| Suspending or deleting a user | Next refresh, or token exp — whichever comes first |
| Removing a user from a group | Next refresh (the groups claim is re-evaluated then) |
| Revoking an admin role | Immediately for ScaiKey's admin API; at refresh for other resource servers |
| Revoking a refresh token | Refresh chain dies immediately; already-issued access tokens live until exp |
| Disabling an application | New token requests fail; in-flight access tokens live until exp |
Worst-case staleness equals the audience application's token_lifetime — 1 hour by default, configurable down to 5 minutes. For an urgent revocation, revoke the ScaiGrid API key (immediate) rather than relying on identity-side changes.
Appendix A — Quick reference#
ScaiKey#
| Purpose | Call |
|---|---|
| Create tenant | POST /api/v1/admin/tenants/ |
| Update tenant settings | PATCH /api/v1/admin/tenants/{tenant_id} |
| Create user in tenant | POST /api/v1/tenants/{tenant_id}/users |
| Grant admin role | POST /api/v1/admin/admin-roles/ |
| List a user's admin roles | GET /api/v1/admin/admin-roles/user/{user_id} |
| Revoke admin role | DELETE /api/v1/admin/admin-roles/{role_id} |
| Set user status | PUT /api/v1/tenants/{tenant_id}/users/{user_id}/status |
| Reset password | POST /api/v1/tenants/{tenant_id}/users/{user_id}/password/reset |
| List applications in a tenant | GET /api/v1/admin/applications?tenant_id={id} |
| Apps a user can use (direct + group) | GET /api/v1/apps/users/{user_id}/applications?tenant_id={id} |
| Audit trail | GET /api/v1/admin/audit |
ScaiGrid#
| Purpose | Call |
|---|---|
| Confirm tenant synced from ScaiKey | GET /v1/tenants (do not POST in this flow) |
| Apply template | POST /v1/tenant-templates/{template_id}/apply/{tenant_id} |
| Map ScaiKey group → role | POST /v1/role-mappings |
| Set user roles | PUT /v1/users/{user_id}/roles |
| Create model group | POST /v1/model-groups |
| Set model access | POST /v1/model-access |
| Create API key | POST /v1/api-keys |
| Revoke API key | DELETE /v1/api-keys/{key_id} |
| Current identity | GET /v1/me |
| Suspend tenant | PUT /v1/tenants/{tenant_id} with status: suspended |
Error codes worth recognising#
| Code | Meaning |
|---|---|
403 AUTHZ_PERMISSION_DENIED |
Caller lacks a permission. Deliberately does not say which one — inspect the user's permissions directly. |
403 TENANT_SUSPENDED |
Tenant (or its partner) is suspended |
401 AUTH_TOKEN_INVALID |
Signature invalid or expired |
403 Platform token requires admin:read or admin:write scope |
ScaiKey service app's allowed_scopes is missing them |
429 RATE_LIMITED |
Rate limit hit; honour Retry-After |