Platform tenant API
The platform tenant API manages tenants without shell access to the API container: the same steps as
ulams:tenant:create, set-env and delete (Tenants), behind
/api/platform/*. The decision is ADR 0078, the details are in
ADR 0085.
Who can call it
Section titled “Who can call it”-
A user of the platform with the
platform_adminpermission (the platform’sadminrole has it afterAuthPermissionSeederran; run it once on an existing platform:php artisan db:seed --class=PermissionsSeeder). -
A scoped token needs
platform:readfor reads andplatform:writefor changes, and lasts at most 30 days on a platform host (scoped tokens). Create one from a platform login:Terminal window printf '%s' "$PASSWORD" | ulams login --url http://api.localhost --email admin@ulams.app --password-stdinulams tokens create --name platform-ci --scopes platform:write --expires-in-days 7 --json -
Calls are throttled to 60 per minute per user (Rate limits) and, with a scoped token, recorded in the agent audit log.
With the CLI
Section titled “With the CLI”ulams tenants list --fields slug,status,urls.frontulams tenants get coffeeulams tenants create acme --name "Acme Academy" --theme coffee --accent "#C2552D" --users 3 --demo # waits, prints stepsulams tenants create acme --no-wait # returns tenant-op:<id>ulams operations wait tenant-op:01j9z3k8m2x4q7r5t6v8w0y1abulams tenants set-env acme --override AI_DRIVER=fake # values are never shown backulams tenants delete acme --yes # permanent: database, files, settingsulams tenants operations listA login on a platform host saves a platform profile. The commands exit 12 (FEATURE_DISABLED) with the
variable to set when the host answers 404.
With curl
Section titled “With curl”Send requests to the platform host (api.localhost in the supplied stack) with a platform token:
TOKEN=ulams_pat_... # platform:read or platform:write, see aboveH=(-H "Authorization: Bearer $TOKEN" -H 'Accept: application/json')
curl -s "${H[@]}" http://api.localhost/api/platform/tenants
curl -s "${H[@]}" -H 'Content-Type: application/json' -H 'Idempotency-Key: acme-create-1' \ -X POST http://api.localhost/api/platform/tenants \ -d '{"slug":"acme","name":"Acme Academy","theme":"coffee","accent":"#C2552D","users":3,"demo":true}'# 202 {"success":true,"message":"Tenant creation queued","data":{"operation":{"id":"01j9...","status":"queued",...},"tenant":{...}}}
curl -s "${H[@]}" http://api.localhost/api/platform/operations/<operation id> # poll until succeeded or failedEndpoints
Section titled “Endpoints”| Request | Does |
|---|---|
GET /api/platform/tenants, GET /api/platform/tenants/{slug} |
List, show: status, URLs, finished steps, names of the setting overrides. Never a database password, key or override value. |
POST /api/platform/tenants {slug, name?, theme?, accent?, users?, demo?} |
Queue creation. 202 with {operation, tenant}. 409 if the tenant is active or an operation for it runs; a failed tenant is resumed. 422 for an invalid slug. |
PATCH /api/platform/tenants/{slug}/env {set?: {KEY: value}, unset?: [KEY]} |
Override or reset inheritable settings (ANTHROPIC_API_KEY, AI_DRIVER, AI_MODEL_*). 422 for any other key. |
DELETE /api/platform/tenants/{slug} {confirm: "<slug>"} |
Queue deletion. 422 unless confirm equals the slug. |
GET /api/platform/operations, GET /api/platform/operations/{id} |
Latest 50, or one: status (queued, running, succeeded, failed), steps with name, status, times and error, and error. |
Provisioning runs as a queued job on the platform queue worker (the supplied stack has one) and takes about a
minute: database, bucket, env, migrate, passport_keys, passport_client, permissions, lti_keys,
demo. A step that failed is kept on the operation; asking for the tenant again continues from it. Deletion has
the steps database, bucket, env, redis.
Common errors
Section titled “Common errors”| Status | Cause |
|---|---|
404 Not found. |
TENANCY_PLATFORM_API is not true, or the host is a tenant host (checked before authentication) |
| 401 | No or invalid token |
| 403 | The user lacks platform_admin, or the scoped token lacks platform:read / platform:write (scope_missing) |
404 Tenant not found. |
Unknown slug on GET, PATCH or DELETE |
409 operation_in_progress (body has data.operation) |
An operation for this tenant is queued or running |
| 409 | POST for a tenant that is already active |
| 422 | Invalid slug, name, theme (^[A-Za-z0-9_-]{1,40}$), accent (#RRGGBB) or users (0 to 50, default 5); an env key that is not inheritable; confirm is not the slug |
| 429 | More than 60 requests per minute |