Cross-diff of all @Controller routes vs docs-json (486 vs 438 after shape normalization): real undocumented routes include the entire authority-matrix family (live 200, 0 doc entries), select-visibility, plugin runtime/proxy, ShareDB socket internals, oauth-server, health; ~27 doc entries belong to the enterprise backend and 404 on this CE instance. Completeness check = controller grep, docs-json alone is not exhaustive.
227 lines
12 KiB
Markdown
227 lines
12 KiB
Markdown
# REST API Reference (direct HTTP)
|
|
|
|
How to call the inkreach-teable backend API directly with plain HTTP (curl /
|
|
node:http / axios / any language) — no CLI involved. The CLI wraps exactly
|
|
these routes, so everything it does is reproducible raw.
|
|
|
|
**Use direct API when**: integrating external scripts/services, the CLI lacks
|
|
an endpoint (auth signup, share views…), writing automation script actions
|
|
that call back into the instance, or working from a language/runtime without
|
|
the CLI. For one-off table/automation operations, prefer the CLI.
|
|
|
|
## 1. Base URL & Authentication
|
|
|
|
Two origins — pick by where you run:
|
|
|
|
| Origin | API (`/api/*`) | Swagger (`/docs`, `/docs-json`) |
|
|
|---|---|---|
|
|
| Public domain, e.g. `https://table.inkreach.cc` | ✅ works (proxied to backend) | ✅ works (frontend rewrite, since 2026-09-23) |
|
|
| Backend origin, e.g. `http://localhost:3003` (reference deployment) | ✅ | ✅ |
|
|
|
|
```bash
|
|
BASE=https://table.inkreach.cc # or http://localhost:3003 on the backend host
|
|
TOKEN=teable_xxx_yyy # PAT; web UI Settings → Personal Access Tokens
|
|
curl -s "$BASE/api/auth/user" -H "Authorization: Bearer $TOKEN" # verify → 200 + user JSON
|
|
```
|
|
|
|
- Every authenticated call: `Authorization: Bearer <PAT>` (+ `Content-Type:
|
|
application/json` on bodies). PAT scopes ∩ base role = effective permission.
|
|
- Discover the configured base/token the CLI already uses:
|
|
`inkreach-teable auth status`, or read `~/.inkreach-teable/config.json`.
|
|
- ⚠️ **Node v24 global `fetch()` (undici) can silently drop the Authorization
|
|
header** — use `curl`, `node:http`, or `axios` instead (details in
|
|
[development.md](../guides/development.md)).
|
|
|
|
## 2. Endpoint Map
|
|
|
|
Verified route-for-route against `src/lib/client.ts` (the CLI's HTTP layer).
|
|
|
|
### Records
|
|
|
|
| Method & route | Purpose |
|
|
|---|---|
|
|
| `GET /api/table/{tid}/record` | List/query records (params below) |
|
|
| `POST /api/table/{tid}/record` | Create one (`{fields}`) or many (`{records:[{fields}]}`) |
|
|
| `PATCH /api/table/{tid}/record` | Batch update — `{records:[{id, fields}]}` |
|
|
| `GET /api/record/{rid}` | Fetch one record |
|
|
| `PATCH /api/record/{rid}` | Update one — `{fields}` |
|
|
| `DELETE /api/table/{tid}/record?recordIds=recA&recordIds=recB` | Delete by ids |
|
|
|
|
List-query params (objects are passed **JSON-stringified**; repeated keys use
|
|
`key[]`): `filter` (JSON), `projection` (JSON array), `search[]` (keyword —
|
|
MUST be the `search[]` tuple form, plain `search=` is ignored), `skip` / `take`
|
|
(pagination; no `orderBy` on this fork — sort via view or `sql`). Response:
|
|
`{ records: [{id, fields}], total? }`.
|
|
|
|
### Fields
|
|
|
|
| Method & route | Purpose |
|
|
|---|---|
|
|
| `GET`/`POST` `/api/table/{tid}/field` | List / create |
|
|
| `GET`/`PATCH`/`DELETE` `/api/table/{tid}/field/{fid}` | Read / update / delete |
|
|
| `PUT /api/table/{tid}/field/{fid}/convert` | Change type **or options** (PATCH strips options!) |
|
|
| `GET /api/table/{tid}/select-visibility/{rid}/{fid}` | Choices visible to a given row (visibilityFilter) |
|
|
|
|
### Tables / Views / Presets
|
|
|
|
| Method & route | Purpose |
|
|
|---|---|
|
|
| `GET`/`POST` `/api/base/{bid}/table` | List / create (body `{name, fields:[…]}`) |
|
|
| `GET`/`PUT` `/api/table/{tid}`, `DELETE /api/base/{bid}/table/{tid}` | Read-update / delete table |
|
|
| `GET`/`POST` `/api/table/{tid}/view`, `GET`/`PATCH`/`DELETE` `/api/view/{vid}` | View CRUD |
|
|
| `GET`/`POST` `/api/table/{tid}/view-preset`, `PUT`/`DELETE` `…/view-preset/{pid}` | Shared view presets |
|
|
|
|
### Automation (self-built engine — `bid` = base id)
|
|
|
|
| Method & route | Purpose |
|
|
|---|---|
|
|
| `GET`/`POST` `/api/base/{bid}/workflow` | List / create (`{name, description?}`) |
|
|
| `GET` `/api/base/{bid}/workflow/{wid}` or `/api/base/any/workflow/{wid}` | Read one (any-variant works cross-base) |
|
|
| `PATCH`/`DELETE` `/api/base/{bid}/workflow/{wid}` | Update metadata / soft delete |
|
|
| `POST` `…/workflow/{wid}/activate` \| `…/deactivate` | Activate (creates snapshot) / deactivate |
|
|
| `PUT` `…/workflow/{wid}/node` | **Whole-graph** node replacement (`{nodes:[…]}`) |
|
|
| `POST` `…/workflow/{wid}/test` | Dry-run → `{nodeResults}` |
|
|
| `GET`/`POST` `…/workflow/{wid}/secret`, `PUT`/`DELETE` `…/secret/{sid}` | Secrets (write-only values) |
|
|
| `GET` `…/workflow/{wid}/execution`, `GET` `…/execution/{eid}`, `POST` `…/execution/{eid}/rerun` | Run history / rerun |
|
|
| `POST` `/api/table/{tid}/record/{rid}/{fid}/button-click` | Fire a button field |
|
|
| `POST` `/api/webhook/{bid}/{wid}` | Fire a webhook (⚠️ real route — the UI shows `/api/webhook/base/{bid}/workflow/{wid}` which is WRONG; `authorization` is never validated) |
|
|
|
|
### Misc
|
|
|
|
| Method & route | Purpose |
|
|
|---|---|
|
|
| `POST /api/base/{bid}/sql` | Read-only SQL (`{sql}`; JOIN/GROUP BY; no record ids) |
|
|
| `GET /api/table/{tid}/aggregation/row-count` | Total row count (`{"rowCount":N}` — verified) |
|
|
| `GET /api/table/{tid}/aggregation…` | Stats family (group-points, calendar-daily, search-count…) — see swagger for params |
|
|
| `GET`/`POST` `/api/access-token`, `DELETE /api/access-token/{id}` | PAT management |
|
|
| `POST /api/auth/signup` | Create account (open on this instance) |
|
|
|
|
## 3. Canonical Examples
|
|
|
|
```bash
|
|
BASE=http://localhost:3003; AUTH="Authorization: Bearer $TOKEN"
|
|
|
|
# create records (single + bulk). typecast: accepts strings for non-text
|
|
# fields; fieldKeyType 'id' = keys are fld ids ('name' = field names)
|
|
curl -s -X POST "$BASE/api/table/tblXXX/record" -H "$AUTH" -H 'Content-Type: application/json' \
|
|
-d '{"fieldKeyType":"id","typecast":true,"records":[{"fields":{"fldTitle":"hello","fldStatus":"Todo"}}]}'
|
|
|
|
# batch update — each record needs its id
|
|
curl -s -X PATCH "$BASE/api/table/tblXXX/record" -H "$AUTH" -H 'Content-Type: application/json' \
|
|
-d '{"fieldKeyType":"id","typecast":true,"records":[{"id":"recXXX","fields":{"fldStatus":"Done"}}]}'
|
|
|
|
# query with filter (JSON must be url-encoded), pagination
|
|
FILTER='{"conjunction":"and","filterSet":[{"fieldId":"fldStatus","operator":"is","value":"Todo"}]}'
|
|
curl -s -G "$BASE/api/table/tblXXX/record" -H "$AUTH" \
|
|
--data-urlencode "filter=$FILTER" --data-urlencode "take=50" --data-urlencode "skip=0"
|
|
|
|
# change field options — MUST go through convert (PATCH strips options)
|
|
curl -s -X PUT "$BASE/api/table/tblXXX/field/fldXXX/convert" -H "$AUTH" -H 'Content-Type: application/json' \
|
|
-d '{"type":"singleSelect","options":{"choices":[{"name":"Todo"},{"name":"Done"}]}}'
|
|
|
|
# automation: create → wire nodes → activate. Node JSON uses the SELF-BUILT
|
|
# contract: trigger "conditions" (not filter), "fieldIds" (not watchFieldIds),
|
|
# category "recordCondition" (not recordMatchesConditions) — full shapes in
|
|
# trigger-types.md / action-types.md
|
|
curl -s -X POST "$BASE/api/base/bseXXX/workflow" -H "$AUTH" -H 'Content-Type: application/json' \
|
|
-d '{"name":"On Go click"}'
|
|
curl -s -X PUT "$BASE/api/base/bseXXX/workflow/wflXXX/node" -H "$AUTH" -H 'Content-Type: application/json' \
|
|
-d '{"nodes":[{"type":"trigger","data":{"triggerType":"buttonClick","tableId":"tblXXX","fieldIds":["fldXXX"]}},{"type":"action","data":{"actionType":"script","code":"return triggerInput.record.fields"}}]}'
|
|
curl -s -X POST "$BASE/api/base/bseXXX/workflow/wflXXX/activate" -H "$AUTH"
|
|
```
|
|
|
|
Inside an automation **script action**, don't hardcode the token — call back
|
|
with the injected values:
|
|
|
|
```js
|
|
const res = await fetch(triggerInput._apiBaseUrl + "/api/table/tblB/record", {
|
|
method: "POST",
|
|
headers: { Authorization: "Bearer " + triggerInput._accessToken, "Content-Type": "application/json" },
|
|
body: JSON.stringify({ records: [{ fields: { fldLog: "done" } }] }),
|
|
});
|
|
```
|
|
|
|
## 4. Gotchas (read before debugging a silent no-op)
|
|
|
|
1. **`typecast:true` does NOT create select choices.** An invalid
|
|
singleSelect string resolves to null and the update becomes a **silent
|
|
no-op** (`mutationApplied=false`): HTTP 200, response echoes the OLD value,
|
|
`__last_modified_time` frozen. Always write existing choice values — check
|
|
`field get` / select-visibility first.
|
|
2. **400 "does not meet the current row conditions"** — value rejected by the
|
|
choice's `visibilityFilter`. Query
|
|
`GET /api/table/{tid}/select-visibility/{rid}/{fid}` for the allowed set.
|
|
3. **Field `options` only via the convert route** — plain `PATCH field` strips
|
|
them.
|
|
4. **`PUT …/node` is whole-graph replacement** — GET current nodes, modify,
|
|
PUT all of them back. Node edits take effect only after re-`activate`
|
|
(`hasDraft: true` = unactivated changes).
|
|
5. **Response-error shape**: non-2xx returns `{status, code, message}`;
|
|
`401` = bad/expired token, `403` = scope or base-role too low.
|
|
6. To see the exact request the CLI sends for any operation, run it with
|
|
`--debug` (prints method/url/body) — the fastest way to learn a route.
|
|
|
|
## 5. Full Route Inventory (this guide ≠ the whole API)
|
|
|
|
The backend exposes **~362 paths**; this guide documents the
|
|
agent-operational core (what the CLI wraps). The authoritative full spec is
|
|
the live OpenAPI JSON — served on the backend origin, and on the public
|
|
domain via the frontend rewrite (`/docs`, `/docs-json`; verified 2026-09-23):
|
|
|
|
```bash
|
|
curl -s "$BASE/docs-json" | python3 -c "import json,sys; [print(m.upper(), p) for p,o in json.load(sys.stdin)['paths'].items() for m in o]"
|
|
# browsable UI: $BASE/docs
|
|
|
|
# deployments WITHOUT that rewrite: docs-json only exists on the backend
|
|
# origin — reach it via an ssh tunnel from other machines:
|
|
ssh -L 3003:localhost:3003 <backend-host> # then use http://localhost:3003/docs-json
|
|
```
|
|
|
|
### ⚠️ Swagger is a curated registry, NOT an auto-scan (audited 2026-09-23)
|
|
|
|
The doc is generated from an explicit `registerRoutes()` registry
|
|
(`packages/openapi/src/generate.schema.ts`), so any controller never
|
|
registered there is **invisible in /docs**. Cross-diffing every
|
|
`@Controller` route in the backend source against docs-json found gaps in
|
|
BOTH directions:
|
|
|
|
- **Real routes missing from swagger** (controller-verified, several
|
|
live-200): the whole **authority-matrix** family
|
|
(`/api/base/{bid}/authority-matrix*` — the 4-layer permission system, see
|
|
`docs/authority-matrix.md` in the app repo); **select-visibility**
|
|
(`GET /api/table/{tid}/select-visibility/{rid}/{fid}`); plugin runtime
|
|
(`/api/plugin/{pid}/runtime*`, `upload-bundle`, `bridge`, `plugin-proxy/*`,
|
|
`csp-origins`); ShareDB internals (`…/socket/doc-ids`,
|
|
`…/socket/snapshot-bulk`); oauth-server (`/api/oauth/authorize`,
|
|
`/decision`, `/access_token`); import status; admin ops
|
|
(performance-cache, heap-snapshot); `/health` + `/health/memory`
|
|
(backend origin only — the domain 404s them).
|
|
- **Doc entries with no controller on this CE instance** (~27: `/user-integrations*`,
|
|
`/unsubscribe/*`, `/billing/*`, `/admin/enterprise-license/*`…): registered
|
|
by the shared openapi package but served by the enterprise backend —
|
|
they 404 here.
|
|
|
|
So the completeness check is the **controller grep**, not docs-json.
|
|
|
|
Route groups NOT detailed in this guide (see docs-json for shapes):
|
|
`/api/auth/*` (session signin/signout, password/email flows, oauth),
|
|
`/api/user/*` (profile, lang, timezone, avatar), `/api/space/*` +
|
|
`/api/base/{bid}/node/*` (spaces & folder tree), `/api/share/*` +
|
|
`/api/base/{bid}/share` (public share views, 17 paths),
|
|
`/api/attachments/*` (upload/signature/notify), `/api/comment/{tid}`,
|
|
`/api/notifications/*`, `/api/mail-sender`, `/api/import/*` +
|
|
`/api/export/*` + `/api/base/import*` (CSV/Excel/Airtable),
|
|
`/api/table/{tid}/selection` (paste/copy ops), `/api/table/{tid}/undo-redo`,
|
|
`/api/trash/*`, `/api/template/*`, `/api/pin/*`, `/api/organization/*`,
|
|
`/api/oauth/*`, `/api/admin/*`, `/api/billing`, `/api/instance/usage`,
|
|
`/api/webhook/{bid}` (official teable webhooks — distinct from the
|
|
automation webhook route), plugin UI endpoints (`/api/plugin/*` panels,
|
|
chart, proxy), `/api/v2/integrity`, `/api/v1/ai-proxy`.
|
|
|
|
When you use one of these, verify the exact param shape before calling —
|
|
from docs-json (backend origin / domain / ssh tunnel), or from the backend
|
|
source:
|
|
`grep -rh "@Controller(\|@Get(\|@Post(\|@Patch(\|@Put(\|@Delete(" apps/nestjs-backend/src/features --include='*.controller.ts'`
|
|
in the inkreach-teable app repo. Do not guess key names (the automation
|
|
contract lesson applies instance-wide).
|