AI Setu Docs
Concepts

Agent provisioning

Stand up new billable child tenants with a single authenticated API call — no human, no inbox.

Normal account creation needs a human to read an OTP email. That's a wall for an agent, a CI job, or a fleet that needs to provision its own isolated workspaces on demand. Agent provisioning removes the human from every account after the first: one human-onboarded, payment-backed parent tenant mints a provisioning key once, and from then on an agent can create as many child tenants as it needs with a single API call.

This is Tier-1: operator/internal provisioning. Provisioning keys are minted by an org owner/admin (SETTINGS_UPDATE) for that org's own use — it is not yet a self-serve, publicly documented reseller product.

Minting a provisioning key

A provisioning key (tt_prov_…) is minted from an already-authenticated, cookie-based session — never from a PAT or another provisioning key (that privilege-chaining path is closed):

mutation {
  createProvisioningKey(input: { name: "sandbox-fleet", maxTenants: 50, expiresInDays: 90 }) {
    id
    secret # shown once
  }
}

maxTenants and expiresInDays are both optional; omitting maxTenants means unlimited child tenants for that key. The key carries exactly one scope, provision:write, and is bound to the tenant and user that minted it. Treat it as a high-privilege, server-side-only secret — it can mint billable tenants and must never ship in client code.

Provisioning a tenant

curl https://api.aisetu.ai/provision/tenant \
  -H "Authorization: Bearer $AI_SETU_PROVISIONING_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Child Co",
    "region": "in",
    "billingMode": "delegated"
  }'
{
  "tenantId": "...",
  "workspaceId": "...",
  "apiKey": "tt_live_...",
  "topUpUrl": "https://..."
}

region (eu | in) defaults to the pod's own region if omitted. billingMode defaults to delegated. Secrets in the response are shown once — there is no way to retrieve apiKey again after this call.

Programmatically, the same call is admin.tenants.provision({...}) from @ai-setu/admin, which throws a clear error if the client wasn't constructed with a provisioning key — a PAT alone cannot provision tenants, by design. An MCP provision_tenant tool is also available and registers only when AI_SETU_PROVISIONING_KEY is set on the MCP server; see MCP for the full tool list and setup.

Billing modes

ModeWho pays
delegated (default)The child tenant funds itself via the returned topUpUrl — identical to a normal new org that starts at $0. It has its own Stripe customer and wallet.
pooledThe parent's wallet. The child gets no Stripe customer and no top-up link (topUpUrl is null); top-ups on the child are rejected with a message pointing at the parent. Usage is still recorded and reported per child tenant and workspace.

Pooled billing

Use pooled when every end-customer needs its own organisation but you want one billing account. (If one organisation with a workspace per customer is enough, those workspaces already share one wallet.)

POST /provision/tenant
Authorization: Bearer tt_prov_…
{ "name": "Tenant 42", "billingMode": "pooled" }

Each request a pooled child makes is reserved against and debited from the parent's wallet; the usage record keeps the child's tenant and workspace, and the parent's ledger rows are tagged with the child they were for. The response's balance headers describe the parent's wallet.

Manage children with GraphQL on /graphql (a PAT with billing:manage; the tenantId argument is your own organisation as the parent), or the admin SDK:

GraphQL@ai-setu/admin
childOrgs(tenantId, first, after, from, to)admin.childOrgs.listPage / iterateYour child orgs with per-child usage for the period (default: this UTC month to date).
parentUsageRollup(tenantId, from, to)admin.childOrgs.usageRollupOne row per organisation + workspace across you and all your children.
adoptChildOrg(tenantId, input)admin.childOrgs.adoptAttach an existing organisation (you must be its owner), or switch a child between POOLED and DELEGATED.
detachChildOrg(tenantId, childTenantId)admin.childOrgs.detachThe child becomes a standalone, self-funded organisation.
import { AiSetuAdmin } from '@ai-setu/admin';
const admin = new AiSetuAdmin(); // AI_SETU_PAT
await admin.childOrgs.adopt(parentTenantId, {
  childTenantId,
  billingMode: 'POOLED',
  transferBalance: true, // move the child's remaining balance into your wallet
});
for (const row of await admin.childOrgs.usageRollup(parentTenantId)) {
  console.log(row.tenantName, row.workspaceName, row.totalMicros);
}

Rules:

  • Pooling an organisation whose wallet is not empty requires transferBalance: true; the balance moves into the parent's wallet in the same transaction (the total across both wallets is unchanged).
  • One level deep: a pooled child cannot pay for pooled children of its own. Parent and child must be in the same region.
  • Changes reach the gateway within about 10 seconds.
  • Spend caps bind to the child: a budget on the child's workspace keeps working under pooled billing, so with one workspace per customer it is the per-customer cap.

Quotas

maxTenants on the key (if set) is enforced atomically per key: the slot is reserved in a single guarded update before any tenant is created, and handed back if creation fails. Concurrent calls can never overshoot the limit, and only tenants created by that specific key count, never a parent-wide or cross-key total. The N+1 call over the limit is refused with QUOTA_EXCEEDED before anything is created.

Limits

LimitValueWhen exceeded
POST /provision/tenant rate, per provisioning key60 / minute and 600 / hour (self-hosted: PROVISION_RATE_LIMIT_PER_MINUTE, PROVISION_RATE_LIMIT_PER_HOUR)429 with Retry-After (seconds). Nothing is created. Keyed by the key id, not the IP.
maxTenants per keyOptional, up to 100,000QUOTA_EXCEEDED
Workspaces per organisation5,000 live workspaces (self-hosted: MAX_WORKSPACES_PER_TENANT)QUOTA_EXCEEDED, details.reason = "workspace_limit_reached"
createWorkspaces batch1 to 100 items per callVALIDATION_ERROR
workspacesPage page sizedefault 50, max 200clamped to 200
Every API route (global)300 requests / 60 s per user or IP429

POST /provision/tenant is not idempotent: every successful call creates a tenant, so keep your own dedupe key and store the returned tenantId. createWorkspaces is idempotent on slug.

One organisation, many workspaces (backfill)

If you want one workspace per your customer inside a single AI Setu organisation (one account, one invoice, isolation per workspace), you don't need provisioning keys. Use a PAT with workspace:create and settings:update and the bulk workspace API. This is also the path for backfilling existing customers.

Put your tenants in a CSV with a stable slug (the slug is the idempotency key; lowercase letters, digits and hyphens, max 64 characters):

name,slug
Acme Retail,saar-1001
Beta Foods,saar-1002
export AI_SETU_PAT=tt_pat_...
ai-setu workspaces create --from-file=tenants.csv --out=keys.json
echo $?   # 0 = all rows created or already existed, 1 = at least one row failed

The CLI sends batches of 100. Each row is its own transaction, so a failure never aborts the rest, and re-running the same file is safe: a slug that already exists comes back as exists with its workspace id and no new key. --out writes the one-time API keys to a 0600 file instead of printing them. Store them in your secret store straight away; they are never shown again (mint another with ai-setu keys create --workspace=<id> --name=<n>). JSON input ([{"name": "...", "slug": "..."}]) and --no-key are also supported.

The same operation from code:

import { AiSetuAdmin } from '@ai-setu/admin';

const admin = new AiSetuAdmin();
const results = await admin.workspaces.createMany(
  tenants.map((t) => ({ name: t.name, slug: `saar-${t.id}` })),
);
for (const r of results) {
  if (r.apiKey)
    await secretStore.put(tenants[r.index].id, r.apiKey); // shown once
  else if (r.error && r.error !== 'exists') console.error(r.index, r.error);
}

or as GraphQL:

mutation ($input: CreateWorkspacesInput!) {
  createWorkspaces(input: $input) {
    index
    workspaceId
    slug
    apiKey
    error
  }
}

Per-row error is null on success, or one of exists (an idempotent no-op, not a failure), workspace_limit_reached, invalid_name, invalid_slug, slug_unavailable (slug held by a deleted workspace) or internal_error (retry). Run batches sequentially: creation is serialised per organisation, so parallel batches only queue and spend your request budget.

For the ongoing path (a customer signs up in your product), call the same operation with one item. To list every workspace, page with workspacesPage(first, after, search) or for await (const w of admin.workspaces.iterate()); the older unpaginated workspaceList is deprecated and capped at 1,000 rows.

Scope and isolation

  • The provisioning verifier only ever accepts the tt_prov_ prefix; it is a separate credential kind from both workspace API keys (tt_live_ / tt_test_) and PATs (tt_pat_). A tt_prov_ key is rejected on every ordinary workspace/PAT-authenticated route, and a PAT is rejected on POST /provision/tenant.
  • The provision:write scope is checked twice — once when the credential is resolved, once again inside the provisioning service — so a bug in one layer can't turn into a privilege escalation in the other.
  • Every child tenant is tagged with the provisioning key and parent tenant that created it, and every provisioning call writes an audit log entry.
  • Workspaces remember the personal access token that created them. Give a runtime token's role workspace:delete_own instead of workspace:delete: it can then delete only the workspaces it created (its own end tenants), never the platform workspace or another token's. The same rule applies to purgeWorkspaceData, so the token can also erase the data of the workspaces it deleted. Full workspace:delete remains the operator-level grant. This narrows workspace deletion only: the same token still needs settings:update to mint keys, and that permission reaches every workspace's keys and configuration, so run it under a dedicated service member with a custom role.

What's next

Key rotation/revocation UI, an IP allowlist, a max-spend cap, an organisation-level spend cap for pooled children, a console view of child organisations, and parent-side suspend of children are not available yet. (The per-key rate limit and the atomic maxTenants quota have shipped; see Limits.)

On this page