Skip to content

Tenants API

Base URL: http://localhost:3001 (default, configurable via PORT env var)

All endpoints require authentication via X-API-Key or Authorization: Bearer header.

Endpoints

MethodPathDescription
GET/api/v1/tenantsList tenants (cursor pagination)
POST/api/v1/tenantsCreate tenant
POST/api/v1/tenants/batchBatch create (up to 100, atomic)
GET/api/v1/tenants/:idGet tenant
PATCH/api/v1/tenants/:idUpdate tenant
DELETE/api/v1/tenants/:idArchive tenant (soft delete)
POST/api/v1/tenants/:id/moveMove tenant in hierarchy
GET/api/v1/tenants/:id/ancestorsGet ancestors (root to parent)
GET/api/v1/tenants/:id/descendantsGet all descendants
GET/api/v1/tenants/:id/childrenGet direct children
GET/api/v1/tenants/:id/contextResolve full context
GET/api/v1/tenants/:id/exportExport tenant data (GDPR, admin)
POST/api/v1/tenants/:id/purgeHard-delete tenant data (GDPR, admin)
POST/api/v1/tenants/:id/migrate-regionMigrate to region (admin)

List Tenants

GET /api/v1/tenants?cursor=<uuid>&limit=50

Query Parameters:

ParamTypeDefaultDescription
cursorUUIDCursor for pagination (tenant ID)
limitnumber50Max results (1—100)

Response: 200 OK

{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"parent_id": null,
"name": "Acme Corp",
"slug": "acme_corp",
"ancestry_path": "/550e8400-e29b-41d4-a716-446655440000",
"depth": 0,
"config": {},
"metadata": {},
"isolation_strategy": "SHARED_RLS",
"status": "active",
"deleted_at": null,
"created_at": "2024-01-01T00:00:00.000Z",
"updated_at": "2024-01-01T00:00:00.000Z"
}
],
"next_cursor": "uuid | null",
"has_more": false
}

Create Tenant

POST /api/v1/tenants

Body:

{
"name": "Acme Corp",
"slug": "acme_corp",
"parent_id": "uuid (optional)",
"config": {},
"metadata": {},
"isolation_strategy": "SHARED_RLS"
}
FieldTypeRequiredDescription
namestringYesDisplay name (1—255 chars)
slugstringYesURL-safe identifier (^[a-z][a-z0-9_]{0,62}$)
parent_idUUIDNoParent tenant ID (null for root)
isolation_strategyenumNoSHARED_RLS, SCHEMA_PER_TENANT, DB_PER_TENANT
configobjectNoInitial config key-value pairs
metadataobjectNoArbitrary metadata

Response: 201 Created — returns the tenant object.


Get Tenant

GET /api/v1/tenants/:id

Response: 200 OK — returns the tenant object.


Update Tenant

PATCH /api/v1/tenants/:id

Body (all fields optional):

{
"name": "New Name",
"slug": "new_slug",
"config": {"key": "value"},
"metadata": {"key": "value"}
}

Response: 200 OK — returns the updated tenant.


Archive Tenant

DELETE /api/v1/tenants/:id

Soft-deletes the tenant (sets status = 'archived'). Fails with 409 HAS_CHILDREN if the tenant has active children.

Response: 204 No Content


Move Tenant

POST /api/v1/tenants/:id/move

Body:

{
"new_parent_id": "uuid"
}

Moves the tenant (and all descendants) under a new parent. Performs cycle detection and acquires advisory locks.

Response: 200 OK — returns the updated tenant.


Batch Create Tenants

POST /api/v1/tenants/batch

Creates up to 100 tenants in a single atomic transaction. If any tenant fails validation, the entire batch rolls back.

Body:

{
"tenants": [
{"name": "Client A", "slug": "client_a", "parent_id": "uuid"},
{"name": "Client B", "slug": "client_b", "parent_id": "uuid"}
]
}

Response: 201 Created

{
"created": [],
"errors": []
}

Get Ancestors

GET /api/v1/tenants/:id/ancestors

Response: 200 OK — array of tenant objects from root to parent.


Get Descendants

GET /api/v1/tenants/:id/descendants

Response: 200 OK — all descendants (uses ltree for efficient subtree query).


Get Children

GET /api/v1/tenants/:id/children

Response: 200 OK — direct children only.


Resolve Context

GET /api/v1/tenants/:id/context

Returns the full tenant context including resolved config and permissions.

Response: 200 OK

{
"tenant_id": "uuid",
"ancestry_path": "/uuid1/uuid2",
"depth": 1,
"isolation_strategy": "SHARED_RLS",
"resolved_config": {
"max_users": {
"key": "max_users",
"value": 500,
"source_tenant_id": "uuid",
"inherited": true,
"locked": false
}
},
"resolved_permissions": {
"manage_users": {
"key": "manage_users",
"value": true,
"mode": "LOCKED",
"source_tenant_id": "uuid",
"locked": true,
"delegated": false
}
}
}

Error Responses

CodeStatusDescription
TENANT_NOT_FOUND404Tenant ID does not exist
TENANT_ARCHIVED410Tenant has been archived
HAS_CHILDREN409Cannot archive tenant with active children
CYCLE_DETECTED409Move would create a cycle
CONFLICT409Slug already exists
VALIDATION_ERROR400Invalid request body

All errors follow this format:

{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message"
}
}