Build with the REST API
List channels and query your Twin from your own application.
Use the public REST API for a service or client that does not speak MCP. Its query surface matches the CLI and MCP: discover accessible channels, inspect their schemas, and either ask the Twin a question or submit a typed query.
Before you begin
An operator must pre-register your OAuth client with exact redirect URIs, the gateway audience, and only the scopes it needs:
twin.querytwin.readtwin.writeonly if users can publish channel posts or replies, create records, manage artifacts, or publish view updates
Your application must complete the OAuth authorization-code flow with PKCE for a real user. The service derives the tenant, organisation, and user from that identity.
Discover channels
List the channels available to the signed-in user:
curl -fsS https://<gateway>/api/twin/channels \
-H "authorization: Bearer $ACCESS_TOKEN"Inspect one channel before constructing a typed query. Channel and saved-view path parameters accept either the returned ID or slug:
curl -fsS https://<gateway>/api/twin/channels/<channel-id> \
-H "authorization: Bearer $ACCESS_TOKEN"The inspection response contains Data Sets and their queryable field keys. A
Data Set owns one schema and its records; its saved Views only add a renderer,
filters, and display configuration. The stable JSON names remain objectType
and objectTypeId for compatibility. Public responses omit tenant metadata,
source relation names, dataset IDs, and other internal implementation details.
List saved Views, then read any of the six renderer types through the canonical data endpoint:
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/views \
-H "authorization: Bearer $ACCESS_TOKEN"
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/views/<view-id>/data \
-H "authorization: Bearer $ACCESS_TOKEN"The response comes from the same saved definition and data engine as the web app. Table, Board, Calendar, and Timeline Views return bounded records. Dashboards return widget results. Pivot tables return their live period columns, rows, and optional totals. Clients do not reconstruct saved filters, field projection, or renderer results.
Temporary filters use the same typed shape as saved View filters and are ANDed with them. Record Views also accept a field projection, limit, and opaque cursor. Encode arrays as JSON query values:
curl -fsSG https://<gateway>/api/twin/channels/<channel-id>/views/<view-id>/data \
-H "authorization: Bearer $ACCESS_TOKEN" \
--data-urlencode 'filters=[{"fieldKey":"status","op":"eq","value":"active"}]' \
--data-urlencode 'fields=["id","title","status"]' \
--data-urlencode 'limit=25'Read and post channel activity
Read recent channel activity in bounded pages. The response mixes posts,
artifact changes, files, and linked sessions and returns nextCursor when
another page is available:
curl -fsS "https://<gateway>/api/twin/channels/<channel-id>/feed?limit=10" \
-H "authorization: Bearer $ACCESS_TOKEN"
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/members \
-H "authorization: Bearer $ACCESS_TOKEN"
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/posts/<post-id>/replies \
-H "authorization: Bearer $ACCESS_TOKEN"
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/feed/threads/session/<session-id> \
-H "authorization: Bearer $ACCESS_TOKEN"The member response contains canonical IDs for active human members explicitly attached to the channel. A shared channel may also be visible to organisation users who are not explicit members. Use only trusted returned IDs for mentions.
An approved top-level post, reply, or thread comment uses the same structured
mention shape. Include mentionedUserIds for people, mentionedChannelIds for
accessible channels, mentionedDataSetIds for Data Sets in the current channel,
and mentionedViewIds for active saved views. Resolve canonical IDs before
writing; resource mentions render as direct links in MLX:
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/posts \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{"body":"@Alex Review @Finance @Invoices @Weekly close.","mentionedUserIds":["<user-id>"],"mentionedChannelIds":["<mentioned-channel-id>"],"mentionedDataSetIds":["<data-set-id>"],"mentionedViewIds":["<view-id>"]}'
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/posts/<post-id>/replies \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{"body":"Reviewed and approved."}'Every post, session, artifact, or file can be the root of one flat discussion.
Create a comment or reaction with its subjectType and subjectId:
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/feed/threads/artifact/<artifact-id>/comments \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{"body":"@Alex Please review @Invoices.","mentionedUserIds":["<user-id>"],"mentionedDataSetIds":["<data-set-id>"]}'
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/feed/threads/session/<session-id>/reactions \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{"reaction":"eyes","active":true}'Edit a top-level post with PATCH or soft-delete it with DELETE at
/api/twin/channels/<channel-id>/posts/<post-id>. Use the same body and
four mention arrays when editing because an edit replaces the complete structured
mention set. Only the principal that authored the post can change it; deletion
hides the post and discussion while preserving the audit record.
Edit a comment with PATCH or delete it with DELETE at
/api/twin/channels/<channel-id>/feed/comments/<comment-id>. Only its author
can edit or delete it. To react to a comment instead of the thread root,
include its commentId. Reactions are thumbs_up, heart, laugh,
celebrate, eyes, or check; use eyes for seen and check for handled.
Reading does not mark anything seen.
The gateway assigns authenticated author and interface attribution. Do not
accept those fields from application users, and do not publish private
reasoning or noisy tool-by-tool progress. All mutations require twin.write.
Create a native channel record
Request twin.write only for applications that expose an explicit user write
action. Use object and field IDs returned by channel inspection:
curl -fsS https://<gateway>/api/twin/channels/<clients-channel-id>/records \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{
"objectTypeId": "client",
"title": "Acme Ltd",
"values": { "name": "Acme Ltd", "status": "active" }
}'The API creates the same canonical record and audit event as the web app.
Source-backed object types return 422; write the source row upstream instead.
Save or Save as a View
Creating a new View is the API equivalent of Save as. Use the exact
objectTypeId returned for the Data Set and provide the complete renderer
definition:
curl -fsS https://<gateway>/api/twin/channels/<channel-id>/views \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data @view-create.jsonUpdating an existing View is Save. Fetch the View and retain its
currentVersionId. Send that as expectedCurrentVersionId with the complete
replacement definition:
curl -fsS -X PATCH \
https://<gateway>/api/twin/channels/<channel-id>/views/<view-id> \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data @view-update.jsonThe API publishes a new version. It returns 409 Conflict when the expected
version is stale, so clients must re-read and show the user the competing
change rather than silently retrying.
Create and update artifacts
Store complete Markdown or CSV without invoking a model:
curl -fsS https://<gateway>/api/twin/artifacts \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{
"kind":"markdown",
"title":"Weekly brief",
"content":"# Weekly brief\\n",
"channelId":"<channel-id>",
"idempotencyKey":"weekly-brief-2026-07-27",
"clientApp":"claude-code"
}'Use the returned artifact.versionId as optimistic concurrency when replacing
the content:
curl -fsS -X PATCH https://<gateway>/api/twin/artifacts/<artifact-id> \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{
"expectedVersionId":"<version-id>",
"title":"Revised weekly brief",
"content":"# Revised weekly brief\\n",
"idempotencyKey":"weekly-brief-revision-2",
"clientApp":"claude-code"
}'The gateway sets interfaceType; clientApp is optional display attribution
for claude-code, codex, gemini, or chatgpt, not an authority claim.
Create and update are idempotent. A stale expectedVersionId returns 409
instead of overwriting a concurrent revision.
Ask the Twin
curl -fsS https://<gateway>/api/twin/queries \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{
"mode": "session",
"question": "What changed in open opportunities this week?",
"channelId": "<channel-id>"
}'Session queries use the same persisted session router as the web app. Omit
channelId to let the router use the signed-in user's available context. A
viewId may be supplied only with its channelId.
The create response is 202 Accepted. Persist query.id, then poll:
curl -fsS "https://<gateway>/api/twin/queries/$QUERY_ID" \
-H "authorization: Bearer $ACCESS_TOKEN"Treat queued and running as non-terminal. Stop on completed, failed, or
cancelled.
Poll at a bounded interval and surface status changes when your application needs to keep a user informed. The public REST surface does not currently provide an SSE or token stream; it exposes lifecycle status and the final governed result without private reasoning or internal run events.
Run a deterministic query
When the caller already knows the exact shape it needs, it can submit a typed records, aggregate, or saved-view intent instead of prose:
{
"mode": "deterministic",
"channelId": "<channel-id>",
"objectTypeId": "invoice",
"operation": "aggregate",
"groupBy": ["month"],
"metrics": [
{ "alias": "revenue", "function": "sum", "field": "amount" }
],
"filters": [
{ "field": "status", "operator": "eq", "value": "paid" }
],
"orderBy": [{ "field": "month", "direction": "asc" }],
"limit": 24
}Metric functions are count_rows, count_distinct, sum, average,
minimum, and maximum. count_rows omits field; every other function
requires an inspected field key.
The server validates object and field names against the inspected channel, injects organisation scope, and compiles a parameterised query. Tenant, organisation, relation, product, SQL, model, prompt, tools, routing, and capabilities are not caller-controlled fields.
Deterministic queries complete in the create response. They contain bounded
data.rows and a complete marker, so they do not need polling.
A saved-view intent uses operation: "view", channelId, viewId, and an
optional widgetIds array. It deterministically evaluates those saved widgets
through the same data engine as the dashboard endpoint.
Submit feedback
curl -fsS https://<gateway>/api/twin/feedback \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "content-type: application/json" \
--data '{
"description": "The answer used bookings instead of invoiced revenue.",
"queryId": "<routed-query-id>"
}'queryId is optional. When present, it must identify a routed session query
visible to the signed-in user. The response is a narrow acknowledgement; the
stored session/run snapshots remain internal.
Results and interface metadata
A completed session query includes an answer and zero or more authenticated channel links. Neither mode exposes SQL, internal relations, run events, prompts, or model configuration.
The service records api, cli, or mcp as interface metadata. That value is
for audit and observability only: it does not select a router, grant authority,
or change the result policy.
Do not use the health route as proof
GET /health shows only that a load balancer can reach the gateway. A usable
connection must complete sign-in and a real query against accessible Twin
context.