Files
inkreach-teable-cli/skills/inkreach-teable/api-reference/rest-api.md
T

11 KiB

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) ✅ ✅
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).

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

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:

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):

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

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 / 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).