Everything you deploy lives under an app, and every app belongs to one account. Create an account once by signing up, then create an app under it for each project: a blog, a staging copy, a customer's own instance.
Accounts
An account has:
- Members, each a person with a role (
admin,member).account.member.setadds one or changes their role;account.member.removeremoves one.account.invite.createmakes an invite link (returned once);account.invite.acceptjoins the signed-in person to the account that invite names. - Keys, scoped to the whole account (see Keys below).
- Apps, each with its own slug, data, functions, site and keys.
account.create makes an account owned by the signed-in person. account.get reads one; account.list lists the accounts the caller belongs to. account.update renames an account.
Apps (tenants)
Operation names call an app a tenant (tenant.*). Each app has its own database, functions and site, isolated from every other app. Each app is identified by its slug, the name in its host (<slug>.<root>) and in routes like tenant.limits's GET /v1/_tenants/{slug}/limits.
account.app.create makes an app owned by an account and returns its key once. The account's plan sets how many apps it may own (max_apps in the plans file, see Plans and limits): past it the call is refused 409 PLAN_LIMIT with field: "apps", max_apps and apps, and a plan that allows more raises it. An account's own max_apps, set with the system key, stands over its plan's. account.app.list lists an account's apps; account.app.delete deletes one with its data. tenant.usage.self reads an app's own storage, documents, request rate and function limits.
When the account has more apps than its plan allows
An account's apps run in an order the account sets. When its plan allows fewer apps than it owns (a subscription ends or is not renewed, or the account moves to a smaller plan), the first max_apps apps in that order keep running and the rest are paused. A paused app keeps everything: its data, files, keys, functions, versions and schedules. Its storage still counts toward the account's space. Its requests and function calls answer 403 APP_OVER_PLAN, and its schedules skip each run until it runs again (the run is logged as suspended).
The order starts as creation order, oldest first, and a new app joins at the end. A paused app runs again, at once and with nothing lost, when any of these happens:
- the account moves it into the first
max_apps(account.apps.order), - an app ahead of it is deleted,
- the plan allows more apps.
An app suspended for another reason stays suspended: reordering and plan changes never lift that suspension.
account.apps.order (PUT /v1/_accounts/{id}/apps/order, the account's owners and admins and an account key) takes apps: every app id of the account (account.app.list's id), each once, in the order they should run. It answers each app in order with active (inside the first max_apps), and max_apps (0: no limit). account.app.list gives each app its position in the order (0 first), active and paused (paused because of the plan), and the account's max_apps. In the console, the account's Apps page orders the apps by drag and drop or the move buttons, shows a line after the first max_apps, and marks each app below it "Paused: over your plan's N apps". Each order is recorded in the audit trail as the action account.apps.order on the account. account.get answers the same max_apps in force: the account's own, else its plan's, else the server setting (0: no limit).
{"name": "substrio_account_app_list", "arguments": {"id": 1}}
# {"apps":[{"id":7,"slug":"blog","position":0,"active":true,"paused":false,...},
# {"id":9,"slug":"shop","position":1,"active":false,"paused":true,...}],"max_apps":1,"next_cursor":null}
{"name": "substrio_account_apps_order", "arguments": {"id": 1, "apps": [9, 7]}}
# {"apps":[{"id":9,"slug":"shop","active":true},{"id":7,"slug":"blog","active":false}],"max_apps":1}
shop now runs and blog is paused. Calls to a paused app get:
{"error":"This app is paused: ...","code":"APP_OVER_PLAN","status":403,"next_action":"reduce_request_or_increase_limit",
"detail":"This app is paused: the account's plan allows 1 app. Move it into the first 1 on the account's Apps page, or upgrade.",
"max_apps":1}
| Code | Status | When |
|---|---|---|
APP_OVER_PLAN |
403 | any request or function call to an app past the first max_apps of its account's order. detail names the limit and max_apps carries it. Move the app up (account.apps.order), delete an app ahead of it, or move to a plan that allows more |
INVALID_ARGUMENT |
400 | account.apps.order with apps missing an app of the account, naming one twice, or naming an app that is not the account's (field: "apps"). Nothing changes |
Regions
region.choices (GET /v1/_accounts/{id}/regions, any member of the account) lists the regions an app can be created in, each as {"slug","name","country"}. A region is listed only while it is offered and serving apps; the list is empty when none is. Pass a listed slug as region to account.app.create to place the app there:
{"name": "substrio_region_choices", "arguments": {"id": 1}}
# {"regions":[{"slug":"eu-central","name":"Frankfurt","country":"DE"}]}
{"name": "substrio_account_app_create", "arguments": {"id": 1, "slug": "shop", "region": "eu-central"}}
| Code | Status | When |
|---|---|---|
REGION_UNAVAILABLE |
409 | the region is not listed by region.choices: unknown, hidden, or not serving apps yet. Pick a listed one, or leave region out |
REGION_FULL |
409 | every server in the region is at its app limit. Pick another region or try again later |
Keys
| Kind | Scope | Who holds it |
|---|---|---|
| Account key | One account: its members, apps, billing and the apps it owns | a person or service acting for the account |
| App key (app key) | One app: its data, functions, deploys and config | the app itself, embedded in its deploys and MCP clients |
| Agent token | One signed-in user's own access, through MCP or OAuth | an AI agent or OAuth client acting as that user |
An account key is created with account.key.create and administers its account: it creates and deletes apps, manages members and sees billing. An app key is what account.app.create and tenant.key.rotate return: it reads and writes that app's data, deploys its functions, and administers that one app, never another account's. An agent token is minted by exchanging an enrollment code (agent.enroll.create, then agent.enroll.exchange) or through MCP OAuth, and acts as the person it was minted for wherever a session would, scoped to what their role allows; see agents.
A key is shown in full exactly once, at creation or rotation. account.key.list shows a key's id, name, mode, creation time, its own request limits and last use; tenant.key.list shows a key's id, creation time, expiry and state (current, previous, expired); neither ever shows the key itself. account.key.revoke and tenant.key.revoke end a key at once.
Creating an app and getting its key
curl -s -X POST https://api.example.com/v1/_accounts/1/apps \
-H "Authorization: Bearer $SUBSTRIO_ACCOUNT_KEY" \
-H 'content-type: application/json' \
-d '{"slug":"shop"}'
# {"_id":2,"slug":"shop","subdomain":"shop","created":true,"key":"...","key_id":"..."}
The same call over MCP, sent with the account key:
{"name": "substrio_account_app_create", "arguments": {"id": 1, "slug": "shop"}}
Save the key field: it is the app's key, returned once, and every later call to the app (data, functions, deploys) uses it. See getting started for the first deploy, and MCP for connecting an agent to the new app.