POST /v1/provisioning/clients creates a complete client tenant in a single call: an
organization, its workspace, the owner user and membership, and (optionally) an
initial tenant API key and an owner invite link. It is the recommended,
operator-facing path for onboarding a new client. The underlying mechanism is the
onboard_client() SQL function described in Client Provisioning.
Authentication
This endpoint does not use a tenant Bearer JWT or a tenant API key. It authenticates with
a dedicated platform provisioning key, sent as a Bearer token:
Authorization: Bearer clp_admin_...
The server stores only the SHA-256 hash of each valid provisioning key, configured in the
CAUSELOOP_PROVISION_KEY_HASHES environment variable (comma-separated, so keys can be rotated
without downtime). Generate a key and its hash with:
python scripts/gen_provision_key.py
If CAUSELOOP_PROVISION_KEY_HASHES is not configured, the endpoint returns 503 Service Unavailable.
Provisioning keys are platform-level credentials — treat them like root. They are separate from,
and far more powerful than, tenant API keys.
Endpoint
POST /v1/provisioning/clients
Authorization: Bearer clp_admin_...
Idempotency-Key: <uuid> # optional but recommended
Content-Type: application/json
Request body
| Field | Type | Required | Notes |
|---|
organization.name | string | Yes | Display name of the organization. |
organization.slug | string | Yes | URL-safe, globally unique. Must match ^[a-z0-9][a-z0-9-]*$. |
organization.plan | string | No | One of free, starter, growth, enterprise. Defaults to free. |
organization.seats | integer | No | Seat allotment for the organization. |
organization.timezone | string | No | IANA timezone (e.g. America/New_York). |
workspace.name | string | No | Workspace label. Defaults to the organization name. |
owner.email | string | Yes | Owner’s email. Reused if a user with this email already exists. |
owner.name | string | No | Owner’s display name. Defaults to the local part of the email. |
issue_api_key | boolean | No | Whether to mint an initial tenant API key. Defaults to true. |
send_owner_invite | boolean | No | Whether to create an owner invite link. Defaults to true. |
Idempotency
Pass an Idempotency-Key header (a UUID) to make retries safe. A retry with the same key
replays the original response byte-for-byte — including the one-time api_key.secret —
so a dropped connection never loses the key. See Idempotency for
the general mechanism.
Without an Idempotency-Key, re-calling with the same slug and owner is still safe: it
returns the existing tenant with created: false and api_key: null (the secret is never
re-issued).
Example
curl -X POST https://api.causeloop.ai/v1/provisioning/clients \
-H "Authorization: Bearer $CAUSELOOP_PROVISION_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"organization": {"name": "Acme Corp", "slug": "acme", "plan": "growth", "seats": 25, "timezone": "America/New_York"},
"owner": {"email": "owner@acme.com", "name": "Jane Doe"}
}'
Response
On success the endpoint returns the created (or existing) tenant. The api_key.secret is
present only on first creation (or on an idempotent replay) and is shown once — store
it immediately.
{
"created": true,
"organization": {"id": "org_01abc...", "slug": "acme", "name": "Acme Corp", "plan": "growth"},
"workspace": {"id": "ws_01abc...", "name": "Acme Corp"},
"owner": {
"user_id": "usr_01abc...",
"membership_id": "mem_01abc...",
"email": "owner@acme.com",
"role": "owner"
},
"api_key": {
"id": "key_01abc...",
"secret": "clp_sk_...",
"scopes": ["issues:read", "issues:write", "..."],
"note": "Shown once. Store securely; it cannot be retrieved later."
},
"owner_invite": {
"id": "inv_01abc...",
"url": "https://app.causeloop.ai/invite/...",
"expires_at": "2026-06-21T10:00:00Z"
}
}
api_key.secret is returned only once. Causeloop stores only its SHA-256 hash and cannot
recover it. If the secret is lost, rotate it through the tenant API key endpoints rather than
re-provisioning — see Settings → API keys.
Status codes
| Status | Meaning |
|---|
201 Created | A new tenant was provisioned. created: true; api_key.secret present (if issue_api_key). |
200 OK | Idempotent existing tenant — the slug + owner already exist. created: false, api_key: null. |
401 Unauthorized | Missing or invalid provisioning key. |
409 Conflict | The slug exists with a different owner, or an Idempotency-Key was reused with a different request body. |
422 Unprocessable Entity | Request body failed validation (e.g. invalid slug pattern or unknown plan). |
503 Service Unavailable | No provisioning key is configured (CAUSELOOP_PROVISION_KEY_HASHES unset). |