# API

The base URL is `https://app.adoor.halmoslabs.com`. Send `Authorization: Bearer <key>` and a JSON body.

A key comes from one of three places:

- In ADoor, open Access and choose New connection. The key is shown once. This is the usual way.
- An app that other people connect signs them in with OAuth and receives a token for the access they approve.
- A job exchanges a workload token at `POST /oauth/token`, described in [Workload identity](/docs/guides/workload-identity/).

`GET /api/operations` lists the operations this key can use, with their input schemas. `POST /api/operations/<name>` runs one. The body is that operation's input.

The agent tools on HTTP and the CLI include `data.query`, `data.execute`, `context.get`, `history.read`, `history.undo`, `files.upload`, `access.request`, `share.apply`, `organize.apply`, `billing.apply`, `account.apply`, and `data.export`. MCP names the data tools `query`, `execute`, `context`, `history`, `undo`, `import`, `request_access`, and `export`. MCP does not publish `share.apply`, `organize.apply`, `billing.apply`, or `account.apply`. Each of those actions is its own MCP tool: `invite_member`, `resend_invitation`, `revoke_invitation`, `update_member`, `remove_member`, `leave_workspace`, `create_connection`, `revoke_connection`, `update_connection`, `list_access_requests`, `resolve_access_request`, `rename_workspace`, `rename_space`, `move_table`, `add_reference`, `remove_reference`, `list_references`, `get_billing`, `get_usage`, `open_checkout`, `open_billing_portal`, `trim_history`, `get_account`, `rename_account`, `list_invitations`, `accept_invitation`, and `decline_invitation`. Listing sessions, revoking a session, signing out everywhere, changing email, and deleting the account stay on the website and the CLI through `account.apply`. They are not MCP tools. `data.execute` runs SQL against this connection's own ADoor database, including DDL. It is not a shell. HTTP accepts `SELECT` in `data.execute`, paged at 1000 rows with `truncated`, `nextCursor` and `rowCount`. `dryRun: true` rolls INSERT, UPDATE, DELETE and SELECT back and commits nothing. A page without `ORDER BY` a unique key can repeat or skip rows when other writes commit. Pass `workspace` as an id or slug on those operations when this connection can open that workspace. Any other value is the same error as a missing workspace. MCP also lists `spaces_create`, `spaces_archive`, `spaces_unarchive`, and `workspaces_create`. The website still calls the other operations on this same endpoint. Federations and consent stay on the website. `open_checkout` and `open_billing_portal` return a link the user opens. Checkout records a claim, and either call may store a Stripe customer id. No card data is returned, and neither call changes the plan. `trim_history` drops undo images and can't be undone. `data.export` on MCP returns a download link.

These paths call the same operations:

- `GET /api/identity`
- `GET /api/tables`
- `POST /api/query` runs SQL the credential is allowed to run. A SELECT returns at most 1000 rows, with `truncated`, `nextCursor` and `rowCount`. Pass `cursor` to continue. A page without `ORDER BY` a unique key can repeat or skip rows when other writes commit. `GET /api/identity` includes `expiresAt` and `effectiveExpiresAt`. `effectiveExpiresAt` is the earliest of the connection clock, ancestor connection clocks, the access credential, and a browser sign-in when the call has one. A permission grant has no clock of its own. An OAuth access token lasts 1 hour and is refreshed.

## SQL functions and operators

Caller SQL admits an allowlist. A function, operator, or statement shape that is not on it fails with `UNSUPPORTED_SQL` and names the function or operator.

Admitted operators are comparisons (`=`, `<>`, `<`, `>`, `<=`, `>=`), arithmetic (`+`, `-`, `*`, `/`, `%`, `^`), boolean `AND` / `OR` / `NOT`, `BETWEEN`, `IN`, `IS NULL`, `LIKE`, `ILIKE`, `SIMILAR TO`, `IS DISTINCT FROM`, string `||`, and JSON `->`, `->>`, `#>`, `#>>`, `@>`, `<@`, `?`, `?|`, `?&`.

Admitted functions include:

- Aggregates that return one scalar: `count`, `sum`, `avg`, `min`, `max`, `bool_and`, `bool_or`, `stddev`, `variance`, and the other standard one-value aggregates.
- `string_agg`, `array_agg`, `json_agg`, and `jsonb_agg` over a user table, including a query that also has `LIMIT`. A free statement admits four of these growing aggregates, including an aggregate `ORDER BY`. A paid statement admits eight. Any window form, including `OVER ()`, is rejected on both plans.
- Text: `lower`, `upper`, `initcap`, `length`, `char_length`, `octet_length`, `substring`, `substr`, `left`, `right`, `trim`, `btrim`, `ltrim`, `rtrim`, `position`, `strpos`, `starts_with`, `ends_with`, `split_part`, `reverse`.
- Null handling: `coalesce`, `nullif`.
- Date and time: `now`, `current_date`, `current_timestamp`, `extract`, `date_part`, `date_trunc`, `age`, and `to_char` with a short literal format.
- JSON field access and `jsonb_typeof`, `jsonb_array_length`, `jsonb_path_exists`.
- `generate_series` and `unnest` as row sources when the following aggregate stays inside the produced-bytes budget. `generate_series` is limited to 1000000 rows. Literal numeric and timestamp bounds are counted, and integer arithmetic of literals is folded. A subquery, parameter, column, or other expression bound is rejected. A step of zero is rejected.
- `SIMILAR TO`, which PostgreSQL parses as `pg_catalog.similar_to_escape`.
- `row_to_json` of a user-table row.

`WITH RECURSIVE` is not available. `ARRAY (SELECT ...)`, `JSON_ARRAYAGG`, and `JSON_OBJECTAGG` are not available. `JSON_ARRAY(1, 2)` and `JSON_OBJECT('id': id)` of ordinary values are available.

These calls produce a new value and are admitted only when that value, and every copy of it, stays within 1 MiB: `repeat`, `lpad`, `rpad`, `format`, `replace`, `regexp_replace`, `translate`, `overlay`, `concat`, `||`, array constructors, `array_to_string`, `json_build_array`, `jsonb_build_array`, `json_build_object`, `jsonb_build_object`, and `row_to_json` of a subquery. A column of a CTE or subquery keeps the size of the expression that built it, so `WITH a AS (SELECT repeat('x', 1000000) AS v) SELECT v || v FROM a` is rejected. Either plan may include eight stored columns in one produced value. A free plan charges each at 1 MiB and a paid plan charges each at 8 MiB, so `first || ' ' || last` and `jsonb_build_object('id', id, 'name', name)` are admitted, and `repeat` of a stored column is admitted through 8. A ninth is rejected. A free plan limits each written value to 10000 leaf elements and 1 MiB. A paid plan limits each written value to 100000 leaf elements and 8 MiB. Nested arrays and jsonb count every leaf. The check runs before the row is stored, and a type change checks the column again at that plan's cap. A sort, `DISTINCT`, `GROUP BY`, or `UNION` is rejected when estimated width times estimated rows exceeds 64 MiB on a free plan or 256 MiB on a paid plan. Stored columns in that estimate use the plan write cap. An unknown user table contributes 16 rows on a free plan and 8 rows on a paid plan, so `ORDER BY` of `name || '!'` stays available. `format` accepts at most 32 conversions. `replace` and `regexp_replace` accept a short literal replacement and no backreference. A long replacement, a backreference, or a value that grows past 1 MiB is `UNSUPPORTED_SQL`.

A cross join, a comma join, or `JOIN` whose condition is not equality of two plain columns from different inputs is rejected when the product of the inputs' row caps is above 1000000. An unconstrained user table counts as 1000000 rows, so two user tables with no join condition are rejected, and so is a user table crossed with `generate_series(1, 2)`. `adoor_tables` and `adoor_columns` are metadata views and count as one row in that product. `<`, `<>`, `a * 0 = b * 0`, and a filter on one side do not count as that equality. The same product includes every allowlisted set-returning function in a subquery, a CTE, a lateral join, a join input, or the select list. A call bounded by one stored row stays available, including `unnest(tags)`, `regexp_split_to_table(body, ',')`, `unnest(ARRAY[1, 2])`, and `generate_series(1, 3)` from a table. `FROM docs d, unnest(d.tags)` is admitted once when `d.tags` is a stored column of that plain table. A second such call, a cast, an unqualified name, or a CTE or subquery column counts in the row product. Equality does not bound a join when either side expands a stored column. `USING` and `NATURAL` stay available for two plain tables, and so does `a.id = b.id` when neither side is that expansion. `ON true` and `ON a.id = b.id OR true` do not. `string_agg`, `array_agg`, and json aggregates are rejected in any window. The database then explains the statement and rejects a plan that is too wide, too costly, or over 1000000 estimated rows. A free account shares a per-minute budget of statement time across its sessions.
- `GET` and `POST /api/connections`
- `POST /api/connections/<id>/revoke`
- `GET` and `POST /api/sessions`

```sh
curl https://app.adoor.halmoslabs.com/api/operations/spaces.list \
  -H "Authorization: Bearer $ADOOR_KEY" \
  -H 'Content-Type: application/json' -d '{}'
```

The same key is the bearer token for [MCP](/docs/reference/mcp/). [Connect](/docs/connect/) has the steps.
