API Reference
The app-plane REST API your application calls, and the conventions every endpoint shares.
This section documents the API your application calls at runtime. The operation pages are generated from the server's OpenAPI contract, so their paths, parameters, schemas and samples match what the API accepts. This overview covers what they all have in common.
What is here
| Group | Contains | Start with |
|---|---|---|
| Auth | Sign-up, sign-in, tokens, magic links, email codes, Google and GitHub, sessions, linked identities | Sign up, Log in, Refresh |
| Your data | Every table, through the same five operations | List, Create |
| Organizations | Your app's own organizations: membership and invitations | My organizations, Invite |
| Users | Role bindings for your app's users | List roles |
| Billing | Your app's products, orders and subscriptions | Subscribe |
| Agents | Invoke an agent, follow its runs, call its tools directly, MCP | Invoke, Call a tool, MCP |
The tables under Your data are those of the demo app this reference is generated from. Your own app's tables follow exactly the same shape; the console's Code › Docs section opens a live reference for your app's own tables and endpoints, behind the app's docs password.
Base URL and paths
Every path begins with the app's uid:
https://api.authoric.ai/{appUid}/autocrud/{table}- The host is the hosted API,
https://api.authoric.ai. These docs write it as$AUTHORIC_URL. {appUid}is the app's App ID, shown with a copy button in the header of the app's page in the console. A request for one app can reach that app only.{table}is one of your app's tables, by name. The examples use the example app'sinvoices.
Requests and responses are JSON (Content-Type: application/json), except file content and
event streams.
Authentication
| Credential | Sent as | Reaches |
|---|---|---|
| Access token, from sign-up, sign-in, a magic link, an email code, or Google/GitHub | Authorization: Bearer <accessToken> | Auth, Your data, Storage, Organizations, Users, Billing, as the signed-in app user |
| API key, created under App access › API keys and bound to one agent | Authorization: Bearer plak_… (or X-API-Key: plak_…) | Agents: /{appUid}/agent/… and /{appUid}/mcp, as that agent |
Access tokens are short-lived (15 minutes by default). When one expires the API answers 401
AUTH_TOKEN_EXPIRED; exchange the refresh token at
POST /{appUid}/auth/token for a new pair and retry. A refresh
token is single-use. A token pair acts in one of the user's organizations (auth.activeOrgUid),
chosen at sign-in or refresh; a request cannot switch it.
An API key does not open the table API. An agent reaches data through its tools, which are derived
from its roles. Every API-key failure, whether the key is missing, malformed, unknown, revoked or
expired, is the same 401 API_KEY_INVALID.
Either way, a credential only identifies the caller. Whether a call is allowed is decided per request by the caller's roles and policies; see The governance model.
Records are named by uid
- A row is addressed by its
uid:GET,PATCHandDELETE/{appUid}/autocrud/{table}/{rowUid}. - Responses never contain the internal
idor theorg_idscope column. - A foreign key is sent and returned as
<name>_uid:customer_idin the table iscustomer_uidin the API, holding the referenced row's uid. An unknown one is400ROW_REFERENCE_NOT_FOUND. - A body carrying
id,uid,org_idor a raw foreign key column is400ROW_FIELD_NOT_ACCEPTED, and the message names the field to use instead.
Listing, filtering and paging
A table's list operation takes these query parameters:
| Parameter | Format | Notes |
|---|---|---|
where | URL-encoded JSON filter tree | Equality, null, $eq $ne $gt $gte $lt $lte, $in, $between, $contains, $startsWith, combined with $and / $or. A reference is filtered by its uid: {"customer_uid":"<uid>"}. |
order | column.asc,column.desc | Up to 3 columns. id sorts in creation order; uid cannot be sorted on. |
columns | comma-separated names | Only these columns, plus uid. |
include | comma-separated references, without _uid | Up to 3. Each referenced row is read with the caller's own permissions on its table. |
limit | integer, 1 to 1,000 | Page size, 50 by default. |
offset | integer ≥ 0 | Rows to skip. Pair with limit and order. |
after | cursor | Keyset paging in creation order: empty for the first page, then paging.nextCursor. |
total | 1 | Count every matching row into paging.total. |
curl --get "$AUTHORIC_URL/$APP_UID/autocrud/invoices" -H "Authorization: Bearer $TOKEN" --data-urlencode 'where={"status":"approved","amount":{"$gte":1000}}' --data-urlencode 'order=due_at.asc' --data-urlencode 'limit=25'Every list answers { "items": [...], "paging": { "limit", "offset", "count", "total", "hasMore" } },
where count is the rows in this page, and total the rows matching the filter when total=1 was
asked for (otherwise null). A keyset page also carries nextCursor. Only the rows the caller may
read are listed and counted: inside an app, that is the caller's own organization. The older
filter=column:value and sort=column DIR still work.
Two more operations sit beside the list:
GET /{appUid}/autocrud/{table}/aggregate?spec=<JSON>counts, sums, averages, minimums and maximums, grouped, in the database. It takes the same permission as listing.POST /{appUid}/autocrud/{table}/batchcreates, changes or deletes up to 500 rows in one transaction, all or nothing.
Querying covers the filter language, paging and aggregates in full, and Batch writes the batch. For a join or a workflow across tables, add a custom action or a handler.
Changes to a table can also be streamed as server-sent events; see Realtime.
Idempotency
Writes to a table (POST, PATCH, DELETE, and a batch) and agent tool calls (POST /{appUid}/agent/tools/{name}) accept an optional Idempotency-Key header. Use a fresh unique
value, such as a UUID, per logical operation and send the same value on every retry of it.
- The first request runs, and its response is stored for 24 hours.
- A repeat with the same key, path and body returns the stored response without running again.
- The same key with a different body is
409. A repeat while the first is still running is409. - A request that failed may be retried with the same key.
Without the header, a write is an ordinary write.
Errors
A failure returns one envelope:
{
"error": {
"error": "A row with that value already exists.",
"code": "CREATE_DUPLICATE",
"requestId": "3f9a1c7e-2b54-4d8a-9f10-6c2e7b5a4d31"
},
"requestId": "3f9a1c7e-2b54-4d8a-9f10-6c2e7b5a4d31"
}Branch on error.code, never on the message. A plain policy denial is a 403 with no code. The
request id is also in the X-Request-Id header. See
Errors and status codes for what each status means, and the
error-code catalog for every code.
Rate limits and quotas
Requests are rate limited at several levels: per IP address, per app, per account, per API key,
more tightly for unauthenticated traffic, and per email address and IP on the sign-in and code
doors. A limited request gets 429, with RateLimit-Limit, RateLimit-Remaining and
RateLimit-Reset headers and a Retry-After. Wait that long, then retry with backoff. The per-IP
limit answers with the code RATE_LIMITED, and a browser on any website can read that response and
its headers, so a front end can tell "slow down" from a network failure.
Separately from rate limits, an account's plan sets quotas (API requests, egress, storage and
others). Past one, app-plane requests answer 402 SUBSCRIPTION_LIMIT_REACHED, with the metric in
error.details; the console stays reachable so the plan can be changed.
What is not here
- The platform plane. Creating apps, editing schema, writing policies and the rest of what the console does use the console's own API. It is documented as how-to guides, not as a contract, and is not something your application should call.
- Internal tables. An app's database holds machinery Authoric uses to run it: policy rows, handler versions, the realtime outbox. They are not customer API and are left out.
- Storage files and folders. The file API (
/{appUid}/storage/…) is covered in the Storage guide.