REST API
API: Semantic Model
Models, tables, fields, relationships, hierarchies, RLS policies, and SquareX measures. All endpoints require authentication unless noted. 67 endpoints, 7 of them app only: an API key gets 403 there.
GET /api/models
Section titled “GET /api/models”List semantic models
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit |
integer | no | |
offset |
integer | no | |
search |
string | no | |
sort |
"recent" | "name" | "tables" |
no |
Responses: 200 · 400 · 401
POST /api/models
Section titled “POST /api/models”Create a new semantic model
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
description |
string | no | |
sourceId |
string | yes |
Responses: 201 · 400 · 401 · 409
GET /api/models/{id}
Section titled “GET /api/models/{id}”Get semantic model by ID
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 200 · 400 · 401 · 404
PUT /api/models/{id}
Section titled “PUT /api/models/{id}”Update a semantic model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | |
description |
string | no | |
isCertified |
boolean | no | |
certifiedBy |
string | no |
Responses: 200 · 400 · 401 · 404
DELETE /api/models/{id}
Section titled “DELETE /api/models/{id}”Delete a semantic model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 204 — No content · 401 · 404
POST /api/models/{id}/publish
Section titled “POST /api/models/{id}/publish”Publish a model version (snapshot current state)
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
override |
boolean | no | |
overrideReason |
string | no |
Responses: 200 · 400 · 401 · 404
GET /api/models/{id}/relationship-suggestions
Section titled “GET /api/models/{id}/relationship-suggestions”List the relationship suggestions of a model that are waiting for a decision
What suggest-relationships found and nobody has accepted or dismissed, best evidence first. Each one names the one side (the referenced key, which a stored relationship starts from) and the many side (the table holding the key). A pair the model has joined since, or whose tables or fields are gone, is not listed. secondRoute says an active relationship already connects the two tables.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 200 · 400 · 401
POST /api/models/{id}/relationship-suggestions/{suggestionId}/accept
Section titled “POST /api/models/{id}/relationship-suggestions/{suggestionId}/accept”Accept a relationship suggestion: create the relationship
Creates the relationship the suggestion describes with the referenced table as from (cardinality 1:N, or 1:1 when the key holder’s key is its own table’s primary key) and closes the suggestion. When an active relationship already connects the two tables, the new one is created inactive (secondRoute: true): two active routes make the planner refuse a measure across them, so switching the route is the person’s decision. 409 when the model already joins those fields.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | |
suggestionId |
string | yes |
Responses: 201 · 400 · 401 · 409
POST /api/models/{id}/relationship-suggestions/{suggestionId}/dismiss
Section titled “POST /api/models/{id}/relationship-suggestions/{suggestionId}/dismiss”Dismiss a relationship suggestion
Closes the suggestion without creating anything. A later suggest-relationships run does not bring the same field pair back.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | |
suggestionId |
string | yes |
Responses: 204 — No content · 401 · 404
POST /api/models/{id}/suggest-relationships
Section titled “POST /api/models/{id}/suggest-relationships”Suggest relationships for a model from its tables’ keys (rule-based)
The joins the model’s tables imply, best evidence first: a foreign key the source declares (0.98), a column named like another table’s sole primary key (0.95), the \{prefix}_id → id convention (0.9 / 0.7), and last a same-named *_id column in a table whose name holds the prefix (0.6 / 0.5). Pairs the model already joins are left out. Each suggestion’s action names the table that holds the key as source* (the many side) and the table it references as target* (the one side); a stored relationship is the other way round (the referenced table is from, cardinality 1:N). New suggestions are also kept as pending suggestions, once per field pair. The rules read the tables’ keys and names; no model is asked and no credits are used.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 200
GET /api/models/{id}/versions
Section titled “GET /api/models/{id}/versions”List model versions
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Responses: 200
POST /api/models/{id}/versions/{version}/restore
Section titled “POST /api/models/{id}/versions/{version}/restore”Restore the model definition from a version snapshot
400, naming it, when the version holds a calculated field or an add-column step that reads past its own table’s row (a subquery, a query written without SELECT, a function that runs one): a version published before such expressions were refused is not written back.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
string | yes | |
version |
integer | yes |
Responses: 200 · 400 · 401 · 404
GET /api/models/{modelId}/ai-descriptions/drafts
Section titled “GET /api/models/{modelId}/ai-descriptions/drafts”The model’s AI-drafted descriptions waiting for review, or all of them (model editors)
state=pending (the default) lists the drafts waiting for review, table by table; state=all adds the ones approved (published: what was written into the model), rejected or superseded (the table or field got a description another way first).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
state |
"pending" | "all" |
no |
Responses: 200 · 400 · 401 · 403 · 404
POST /api/models/{modelId}/ai-descriptions/drafts
Section titled “POST /api/models/{modelId}/ai-descriptions/drafts”Draft the missing descriptions of a model’s tables and fields with AI (models:create; AI credits)
App only. An API key gets 403: AI features are available in the DataSquares app; API keys can’t use them.
Drafts a description for each table and field in the AI scope that has none, for up to 10 tables a request: the ones named, or the next ones with something to draft. One quick AI call a table, charged in AI credits (Description drafts). Nothing is published: each draft waits for a model editor’s review. dropped: the drafts the checks left out, their text not kept, and droppedFor why (quotedValue: it quoted a code stored in the table’s data; tooLong; screened: it looked like personal details, a secret or an instruction; noAnswer: the AI gave none). A draft that was rejected or dropped is drafted again only for a table named here. One request at a time per model: another one meanwhile is refused with 409 DRAFTS_RUNNING before any AI call, details.running saying whether you started the run, when, and for how many tables. 429 DRAFTS_BUSY, also before any AI call, when the server is drafting for as many models as it runs at once: try again after Retry-After (60 seconds); nothing was drafted or charged. A table is started only while its call can end within 4 minutes of the request’s start: notStarted lists the tables chosen and not started (the next request drafts them; name them again if you named them). remaining: the tables still with something to draft. A caller a row-level security policy restricts on the model’s source gets drafts written without the tables’ counts. 400 when more tables are named than a request drafts, or one is not the model’s or outside its AI scope; 503 AI_NOT_CONFIGURED on a server without AI.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
tableIds |
array of string | no |
Responses: 200 · 400 · 401 · 403 · 404 · 409 · 429 · 503
PUT /api/models/{modelId}/ai-descriptions/drafts
Section titled “PUT /api/models/{modelId}/ai-descriptions/drafts”Approve or reject AI-drafted descriptions (models:update)
approve writes the draft, or text in its place, into the model as the table’s or field’s description, only where it still has none (otherwise the draft is superseded); reject keeps it out. Every draft must be one of this model’s drafts waiting for review (400 otherwise, naming them), and nothing is changed then.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
decisions |
array of object | yes |
Responses: 200 · 400 · 401 · 403 · 404
POST /api/models/{modelId}/ai-detect
Section titled “POST /api/models/{modelId}/ai-detect”Detect relationships between a model’s tables from their keys (rule-based)
The same detection as suggest-relationships, answered directly instead of being stored: from is the table that holds the key, to the table it references. Pairs the model already joins are left out. The rules read the tables’ keys and names; no model is asked and no credits are used, whatever the path says.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401 · 404
GET /api/models/{modelId}/ai-notes
Section titled “GET /api/models/{modelId}/ai-notes”The AI’s notes about a model’s tables: those waiting for the owner’s review, or the shared ones (model editors)
App only. An API key gets 403: AI features are available in the DataSquares app; API keys can’t use them.
Notes the DataSquares AI assistant keeps about this model’s tables (phase 3 of the AI plan): what a table’s rows mean, its key and date column, usual filters, what a column’s codes mean, which joins hold, questions and the tables that answered them. state=pending (the default) lists the notes waiting for the model owner’s review: meanings the AI inferred from people’s answers or found by Explore this model, and people’s own words; they apply only to the person whose notes they are until approved. Only the model’s owner and workspace admins, with permission to update data models, see them (canReview); another editor gets their count and no items. state=shared lists the notes the workspace shares: facts a check of the data proved (shared at once) and notes approved. A note naming a field labelled sensitive is listed only to people who may read labelled values in clear. Oldest first.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
state |
"pending" | "shared" |
no | |
limit |
integer | no | |
offset |
integer | no |
Responses: 200 · 400 · 401 · 403 · 404
GET /api/models/{modelId}/ai-notes/explore
Section titled “GET /api/models/{modelId}/ai-notes/explore”Explore this model: whether it can run, its estimate in AI credits, and its progress (model editors)
App only. An API key gets 403: AI features are available in the DataSquares app; API keys can’t use them.
Explore this model runs the DataSquares AI assistant’s schema finder over every table of the model in your AI scope, in the background, to learn them before the first questions: what each table’s rows mean, its key, its date column, its codes, how the tables join. What it infers waits for the model owner’s review; facts its checks of the data prove are shared at once. estimate: about how many AI credits it uses, whole credits rounded up (it is charged what it used, as one line). job: the running exploration, else the latest, with its progress and what it was charged so far. Nothing here calls the AI.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401 · 403 · 404
POST /api/models/{modelId}/ai-notes/explore
Section titled “POST /api/models/{modelId}/ai-notes/explore”Start Explore this model in the background, its estimate confirmed (models:create; AI credits)
App only. An API key gets 403: AI features are available in the DataSquares app; API keys can’t use them.
Starts the exploration (202) and returns it running; follow it with GET …/ai-notes/explore. confirmCredits: the estimate you were shown, whole credits; when the estimate is now higher (the model grew), 409 ESTIMATE_CHANGED with details.estimateCredits, and nothing starts. Refused before anything runs: the AI policy (403 AI_TURNED_OFF, 403 AI_NOT_ALLOWED, 429 AI_BUDGET_REACHED, also when this month’s budget has less left than the estimate), the workspace’s AI credits (429 CREDITS_EXHAUSTED, past what may go below zero), 409 MEMORY_PAUSED while your memory is paused (what it infers is kept as your own notes about the tables, and a paused memory keeps nothing new), 409 EXPLORE_NOT_AVAILABLE (details.reason, as the GET’s), 409 EXPLORE_RUNNING (details.job) while one runs for the model, 429 EXPLORE_BUSY while the server explores as many models as it runs at once (try again after Retry-After, 60 seconds), 503 AI_NOT_CONFIGURED on a server without AI. It runs as you: your AI scope, your access to labelled fields, your row-level security. It is charged to the workspace’s AI credits as one line, what it used, a stop included.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
confirmCredits |
integer | yes | The estimate you were shown, whole credits |
Responses: 202 · 400 · 401 · 403 · 404 · 409 · 429 · 503
POST /api/models/{modelId}/ai-notes/explore/stop
Section titled “POST /api/models/{modelId}/ai-notes/explore/stop”Stop the model’s running exploration at its next step (models:create)
App only. An API key gets 403: AI features are available in the DataSquares app; API keys can’t use them.
Asks the running Explore this model to stop, wherever it runs; it stops at its next step and keeps what it learned so far. It is charged for what it used. 409 EXPLORE_NOT_RUNNING when none runs.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401 · 403 · 404 · 409
PUT /api/models/{modelId}/ai-notes/review
Section titled “PUT /api/models/{modelId}/ai-notes/review”Approve, reject or remove the AI’s notes about a model’s tables (the model’s owner or a workspace admin)
App only. An API key gets 403: AI features are available in the DataSquares app; API keys can’t use them.
approve shares a note waiting for review with the workspace: everyone it may reach uses it from then on (a note shared on the same thing before is superseded). title and body approve it in your own words instead: checked as every note is (personal or secret details, data values and instructions to the AI are refused, whole, never cut). A note whose table changed since it was written is not approved: it goes back to review (changed). One note is shared about the same thing (the same place): approving two notes of one place in a request is refused (400, naming them in details.notes; details.reason same_place). reject keeps it from everyone, the person whose note it was included; they see it was not approved. remove takes a shared note (a checked fact, an approved note) out of use. Every decision must name one of this model’s notes in the state its action needs, at most 200 a request: all or none (400, naming them in details.notes; details.reason not_waiting, as when another reviewer decided them meanwhile). The person whose note was decided is told, once a review. Needs permission to update data models.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
decisions |
array of object | yes |
Responses: 200 · 400 · 401 · 403 · 404
GET /api/models/{modelId}/ai-scope
Section titled “GET /api/models/{modelId}/ai-scope”The model’s AI scope: which tables and fields the AI may use (model editors)
Every table and field of the model, each with inScope. Everything is in scope until it is excluded, so a table a re-discovery adds is in scope. AI search, its table pick and the AI’s schema tools use only what is in scope; a table or field every model over a source keeps outside it is not read by the AI’s SQL and data tools either.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401 · 403 · 404
PUT /api/models/{modelId}/ai-scope
Section titled “PUT /api/models/{modelId}/ai-scope”Set the model’s AI scope: the tables and fields the AI may not use (models:update)
Replaces the model’s exclusions. Every id must be a table or field of this model (400 otherwise, naming them); a field of an excluded table needs no exclusion of its own and is refused as one (400). The AI catalog follows within minutes: what is excluded is deleted from the AI’s search documents.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
excludedTables |
array of string | yes | |
excludedFields |
array of string | yes |
Responses: 200 · 400 · 401 · 403 · 404
GET /api/models/{modelId}/fields
Section titled “GET /api/models/{modelId}/fields”List all fields of a model (all tables, hidden excluded)
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200
GET /api/models/{modelId}/fields/{fieldId}/usage
Section titled “GET /api/models/{modelId}/fields/{fieldId}/usage”What reads a model field — cards, metrics, measures (field-level lineage)
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
fieldId |
string | yes |
Responses: 200 · 400 · 401 · 404
GET /api/models/{modelId}/hierarchies
Section titled “GET /api/models/{modelId}/hierarchies”List hierarchies in a model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401
POST /api/models/{modelId}/hierarchies
Section titled “POST /api/models/{modelId}/hierarchies”Create a drill-down hierarchy
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
tableId |
string | no | |
levels |
array of object | yes |
Responses: 201 · 400 · 401 · 409
PUT /api/models/{modelId}/hierarchies/{hierId}
Section titled “PUT /api/models/{modelId}/hierarchies/{hierId}”Update a hierarchy
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
hierId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | |
tableId |
string | no | |
levels |
array of object | no |
Responses: 200 · 400 · 401 · 404
DELETE /api/models/{modelId}/hierarchies/{hierId}
Section titled “DELETE /api/models/{modelId}/hierarchies/{hierId}”Delete a hierarchy
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
hierId |
string | yes |
Responses: 204 — No content · 401 · 404
POST /api/models/{modelId}/import-tables
Section titled “POST /api/models/{modelId}/import-tables”Import tables from the data source into the model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
schema |
string | no | Filter by schema name |
tables |
array of string | no | Specific table names to import (imports all if omitted) |
createRelationships |
boolean | no | After the import, create the relationships the source declares (a foreign key) or that a column named like another table’s sole primary key implies. Created parent first (the referenced table is from, cardinality 1:N), flagged ai_detected, and only between tables no active relationship connects yet, so no second route is ever added; weaker matches and the ones left out stay in suggest-relationships. Default false. |
Responses: 200
GET /api/models/{modelId}/measures
Section titled “GET /api/models/{modelId}/measures”List SquareX measures on a model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401
POST /api/models/{modelId}/measures
Section titled “POST /api/models/{modelId}/measures”Create a SquareX measure (validated at save)
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
expression |
string | yes | |
dataType |
"number" | "integer" | "decimal" | "string" | "date" | "boolean" |
no | |
format |
object | null | no | |
folder |
string | null | no | |
description |
string | null | no | |
isHidden |
boolean | no |
Responses: 201 · 400 · 401 · 409
GET /api/models/{modelId}/measures/{mid}
Section titled “GET /api/models/{modelId}/measures/{mid}”Fetch a SquareX measure
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
mid |
string | yes |
Responses: 200 · 400 · 401 · 404
PUT /api/models/{modelId}/measures/{mid}
Section titled “PUT /api/models/{modelId}/measures/{mid}”Update a SquareX measure (revalidates; marks dependents stale)
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
mid |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | |
expression |
string | no | |
dataType |
"number" | "integer" | "decimal" | "string" | "date" | "boolean" |
no | |
format |
object | null | no | |
folder |
string | null | no | |
description |
string | null | no | |
isHidden |
boolean | no |
Responses: 200 · 400 · 401 · 404
DELETE /api/models/{modelId}/measures/{mid}
Section titled “DELETE /api/models/{modelId}/measures/{mid}”Soft-delete a SquareX measure (409 while referenced)
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
mid |
string | yes |
Responses: 204 — No content · 401 · 404
POST /api/models/{modelId}/measures/{mid}/preview
Section titled “POST /api/models/{modelId}/measures/{mid}/preview”Compiled SQL preview of a SquareX measure for a dialect
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
mid |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
dialect |
string | no |
Responses: 200 · 400 · 401 · 404
POST /api/models/{modelId}/measures/copilot
Section titled “POST /api/models/{modelId}/measures/copilot”SquareX copilot: draft a measure from natural language, validated + self-repaired (AIX-18)
App only. An API key gets 403: AI features are available in the DataSquares app; API keys can’t use them.
One AI call drafts an expression from the model’s real schema + glossary + canonical exemplars; the draft runs through the REAL analyzer and SQX0xx diagnostics are fed back for up to 2 repair rounds. ok:true is only returned for a GREEN expression — clients insert nothing else; ok:false is a draft that failed the checks or held no formula. A draft the AI never answered is an error that says why: 429 when the workspace’s AI credits are used up (CREDITS_EXHAUSTED, with details.recharge_path) or its usage limit is reached, 403 for an effort level the workspace lacks, 503 when AI is unavailable or took too long. 503 AI_NOT_CONFIGURED on a server without AI.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
prompt |
string | yes | |
currentExpression |
string | null | no | |
name |
string | null | no | |
measureId |
string | null | no |
Responses: 200 · 403 · 429 · 503
POST /api/models/{modelId}/measures/validate
Section titled “POST /api/models/{modelId}/measures/validate”Validate a SquareX expression (diagnostics, never a 4xx)
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
expression |
string | yes | |
name |
string | no | |
measureId |
string | no |
Responses: 200 · 400 · 401 · 404
GET /api/models/{modelId}/prep-for-ai
Section titled “GET /api/models/{modelId}/prep-for-ai”Prep for AI: how ready the model is for the AI, what to do next, and its joins with their provenance (model editors)
score (0–100) weighs the tables and fields in the AI scope that have a description, the tables a declared or saved-SQL join reaches, and, while the server runs AI search (band is null otherwise and the size is left out), the model’s size (read whole, with a one-line overview of its tables, or too large for either). recommendations: what to do next, with counts: draft_descriptions (tables and fields with no description and no draft yet), review_drafts, describe_left_out (no description, and their draft was rejected or left out by the checks, so it is not drafted again), define_scope, meaning_search, joins (tables no declared or saved-SQL join reaches). joins lists every join of the model’s tables and how it is known: declared (detected: found from the source’s keys and not yet confirmed), saved_sql (used by the workspace’s saved SQL; usage: how many queries), value_match (matched from the values of both sides: confidence and the counts it rests on, never a value; a possible join the AI confirms before using it), ai_suggested (a possible join too). No AI is called.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401 · 403 · 404
GET /api/models/{modelId}/relationships
Section titled “GET /api/models/{modelId}/relationships”List relationships in a model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401
POST /api/models/{modelId}/relationships
Section titled “POST /api/models/{modelId}/relationships”Create a relationship between model tables
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
fromTableId |
string | yes | |
fromFieldId |
string | yes | |
toTableId |
string | yes | |
toFieldId |
string | yes | |
joinType |
"inner" | "left" | "right" | "full" |
no | |
cardinality |
"1:1" | "1:N" | "N:M" |
no | |
isActive |
boolean | no |
Responses: 201 · 400 · 401 · 409
PUT /api/models/{modelId}/relationships/{relId}
Section titled “PUT /api/models/{modelId}/relationships/{relId}”Update a model relationship
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
relId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
fromTableId |
string | no | |
fromFieldId |
string | no | |
toTableId |
string | no | |
toFieldId |
string | no | |
joinType |
"inner" | "left" | "right" | "full" |
no | |
cardinality |
"1:1" | "1:N" | "N:M" |
no | |
isActive |
boolean | no | |
aiDetected |
boolean | no |
Responses: 200 · 400 · 401 · 404
DELETE /api/models/{modelId}/relationships/{relId}
Section titled “DELETE /api/models/{modelId}/relationships/{relId}”Delete a model relationship
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
relId |
string | yes |
Responses: 204 — No content · 401 · 404
GET /api/models/{modelId}/rls
Section titled “GET /api/models/{modelId}/rls”List RLS policies for a model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401
POST /api/models/{modelId}/rls
Section titled “POST /api/models/{modelId}/rls”Create a row-level security policy
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
tableId |
string | yes | A table of this model; the policy filters its rows. |
filterExpression |
string | no | A SQL condition over the table’s own columns, e.g. region = ‘EU’. Rows it does not keep are hidden. Required unless the policy has attribute rules, beside which it is an extra condition. |
appliesTo |
array of object | yes | Who the policy applies to. An empty list applies it to everyone who can query the model, company admins included. Otherwise it applies to anyone matching any target. Several policies on one table combine with OR; a table with no policy that applies to a caller is not filtered for them. A policy binds its data source: it filters its table’s rows through every model over the source that reads the same source table, and the policies written on different model tables combine with AND. Anyone a policy on the source restricts can’t add, change or remove the source’s policies (403); company admins can. A target that can never match is refused (400, details.targets). |
attributeRules |
array of object | no | Rules on the person’s attributes. Each keeps a row only when column holds one of the person’s values for attribute; a person this policy applies to who has no value for a named attribute sees no rows of the table. They AND with filterExpression, which is then optional. An empty list removes the rules. |
Responses: 201 · 400 · 401 · 409
PUT /api/models/{modelId}/rls/{policyId}
Section titled “PUT /api/models/{modelId}/rls/{policyId}”Update an RLS policy
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
policyId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | |
tableId |
string | no | A table of this model; the policy filters its rows. |
filterExpression |
string | no | A SQL condition over the table’s own columns, e.g. region = ‘EU’. Rows it does not keep are hidden. Required unless the policy has attribute rules, beside which it is an extra condition. |
appliesTo |
array of object | no | Who the policy applies to. An empty list applies it to everyone who can query the model, company admins included. Otherwise it applies to anyone matching any target. Several policies on one table combine with OR; a table with no policy that applies to a caller is not filtered for them. A policy binds its data source: it filters its table’s rows through every model over the source that reads the same source table, and the policies written on different model tables combine with AND. Anyone a policy on the source restricts can’t add, change or remove the source’s policies (403); company admins can. A target that can never match is refused (400, details.targets). |
attributeRules |
array of object | no | Rules on the person’s attributes. Each keeps a row only when column holds one of the person’s values for attribute; a person this policy applies to who has no value for a named attribute sees no rows of the table. They AND with filterExpression, which is then optional. An empty list removes the rules. |
Responses: 200 · 400 · 401 · 404
DELETE /api/models/{modelId}/rls/{policyId}
Section titled “DELETE /api/models/{modelId}/rls/{policyId}”Delete an RLS policy
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
policyId |
string | yes |
Responses: 204 — No content · 401 · 404
GET /api/models/{modelId}/rls/attributes
Section titled “GET /api/models/{modelId}/rls/attributes”List the people’s attributes a row-level security policy can name
The attribute names held by the people who can query this model’s workspace (set by a company admin or SCIM), with how many hold each. Never the values. For people who may create or edit models.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401 · 404
POST /api/models/{modelId}/rls/coverage
Section titled “POST /api/models/{modelId}/rls/coverage”Preview who a row-level security policy applies to
What a draft policy would do once saved, evaluated by the same rules queries use: how many of the people who can query the workspace it applies to, and how many would then see every row of the table, only filtered rows, or none, together with the policies already on the table. Each person’s outcome is listed. Nothing is saved.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
tableId |
string | yes | The table the draft policy filters. |
appliesTo |
array of object | yes | The draft’s targets, as for a policy: an empty list is everyone. Unlike a save, a target that can never match is not refused here: it comes back in targets with its status and is counted as queries would read it. |
attributeRules |
array of object | no | The draft’s rules on people’s attributes, checked as a save checks them: who holds a value for every named attribute sees the rows they keep, who does not sees none. |
policyId |
string | no | When editing: the policy the draft replaces, so it is not counted twice. |
list |
"covered" | "not_covered" |
no | Which people to list: those the draft applies to, or the rest. Everyone when absent. |
search |
string | no | Part of a name or email, any case. |
limit |
integer | no | |
offset |
integer | no |
Responses: 200 · 400 · 401 · 404
GET /api/models/{modelId}/rls/inherited
Section titled “GET /api/models/{modelId}/rls/inherited”List the row-level security policies a model inherits from other models over its source
A policy binds its data source: it filters its table’s rows through every model over the source that reads the same source table. These are the active policies of other models over this model’s source that bind its tables, read-only (one is changed on the model it is written on), by model and name: each with the model and the table it is written on, the source table that joins them, and binds, the tables of this model it filters. unmatched: its table can’t be matched to a table, so every query on the source it would narrow is refused.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200 · 400 · 401 · 404
GET /api/models/{modelId}/rls/people
Section titled “GET /api/models/{modelId}/rls/people”List the people a row-level security policy can apply to
Everyone who can query this model’s workspace: its members and the company’s admins, by name. roles counts the people holding each company role a role target can match. For people who may create or edit models.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
search |
string | no | Part of a name or email, any case. |
limit |
integer | no | |
offset |
integer | no |
Responses: 200 · 400 · 401 · 404
GET /api/models/{modelId}/tables
Section titled “GET /api/models/{modelId}/tables”List tables in a model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
includeFields |
boolean | no | Include fields for each table |
Responses: 200 · 400 · 401
POST /api/models/{modelId}/tables
Section titled “POST /api/models/{modelId}/tables”Add a table to the model
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
sourceSchema |
string | yes | |
sourceTable |
string | yes | |
alias |
string | yes | |
description |
string | no |
Responses: 201 · 400 · 401 · 409
GET /api/models/{modelId}/tables/{tableId}
Section titled “GET /api/models/{modelId}/tables/{tableId}”Get a model table by ID
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes |
Responses: 200 · 400 · 401 · 404
PUT /api/models/{modelId}/tables/{tableId}
Section titled “PUT /api/models/{modelId}/tables/{tableId}”Update a model table
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
sourceSchema |
string | no | |
sourceTable |
string | no | |
alias |
string | no | |
description |
string | no |
Responses: 200 · 400 · 401 · 404
DELETE /api/models/{modelId}/tables/{tableId}
Section titled “DELETE /api/models/{modelId}/tables/{tableId}”Delete a model table
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes |
Responses: 204 — No content · 401 · 404
GET /api/models/{modelId}/tables/{tableId}/fields
Section titled “GET /api/models/{modelId}/tables/{tableId}/fields”List fields for a model table
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes |
Responses: 200 · 400 · 401
POST /api/models/{modelId}/tables/{tableId}/fields
Section titled “POST /api/models/{modelId}/tables/{tableId}/fields”Add a field to a model table
409 when the table already has a field by that name, or one that reads a column of that name, ignoring case: a field by that name would take over the charts and filters that read the other. 400 when a calculated field (fieldType calculated, or one with an expression) is given a sourceColumn: it reads no column of its own, and a filter on a column never reads it. 400, naming it, when the expression reads past its own table’s row: a subquery, a query written without SELECT (TABLE, FROM first, a table after IN) or a function that runs one (query_to_xml and the like), which would read the source past row-level security; the same expression is refused when a query reads it. A field added by anyone but a schedule’s creator or a workspace admin pauses the publisher-mode schedules that read the model, for their creators.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
sourceColumn |
string | no | |
fieldType |
"dimension" | "measure" | "calculated" |
yes | |
dataType |
"string" | "number" | "integer" | "decimal" | "boolean" | "date" | "datetime" | "time" | "geo" |
yes | |
aggregation |
"sum" | "count" | "count_distinct" | "avg" | "min" | "max" | "none" |
no | |
expression |
string | no | |
format |
string | no | |
description |
string | no | |
isHidden |
boolean | no | |
sortByField |
string | no | |
semanticType |
string | no | |
isMetric |
boolean | no |
Responses: 201 · 400 · 401 · 409
PUT /api/models/{modelId}/tables/{tableId}/fields/{fieldId}
Section titled “PUT /api/models/{modelId}/tables/{tableId}/fields/{fieldId}”Update a model field
409 when the new name is, ignoring case, another field’s name or the column another field reads in its table; the field’s own name, in any case, is no new name. 400 when sourceColumn is given to a field that is, or with this change becomes, calculated (fieldType calculated, or an expression): it reads no column of its own. 400, naming it, when the expression reads past its own table’s row (a subquery, a query written without SELECT, a function that runs one), as on create. A change by anyone but a schedule’s creator or a workspace admin to what a send of the model shows pauses the publisher-mode schedules that read the model, for their creators.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes | |
fieldId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | no | |
sourceColumn |
string | no | |
fieldType |
"dimension" | "measure" | "calculated" |
no | |
dataType |
"string" | "number" | "integer" | "decimal" | "boolean" | "date" | "datetime" | "time" | "geo" |
no | |
aggregation |
"sum" | "count" | "count_distinct" | "avg" | "min" | "max" | "none" |
no | |
expression |
string | no | |
format |
string | no | |
description |
string | no | |
isHidden |
boolean | no | |
sortByField |
string | no | |
semanticType |
string | no | |
isMetric |
boolean | no | |
ordinal |
integer | no | |
sensitivityLabel |
string | null | no |
Responses: 200 · 400 · 401 · 404 · 409
DELETE /api/models/{modelId}/tables/{tableId}/fields/{fieldId}
Section titled “DELETE /api/models/{modelId}/tables/{tableId}/fields/{fieldId}”Delete a model field
409 while a dashboard card, a report, a dashboard’s filter pane, the filters a schedule sends or a metric names the field by its id (in any spelling of it), or a relationship joins on it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes | |
fieldId |
string | yes |
Responses: 204 — No content · 401 · 404 · 409
GET /api/models/{modelId}/tables/{tableId}/transforms
Section titled “GET /api/models/{modelId}/tables/{tableId}/transforms”List transform steps for a table
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes |
Responses: 200
POST /api/models/{modelId}/tables/{tableId}/transforms
Section titled “POST /api/models/{modelId}/tables/{tableId}/transforms”Add a transform step to a table
400 when the step cannot compile. An add_column expression reads its own row only: one with a subquery, a query written without SELECT (TABLE, FROM first, a table after IN) or a function that runs one (query_to_xml and the like) is refused, naming it, because it would read the source past row-level security; a step stored before is refused when a query reads it.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
type |
"filter_rows" | "remove_columns" | "rename_column" | "change_type" | "add_column" | "replace_values" | "sort_rows" | "top_n" | "deduplicate" | "group_aggregate" |
yes | |
config |
object | yes | |
label |
string | no |
Responses: 200
PUT /api/models/{modelId}/tables/{tableId}/transforms/{transformId}
Section titled “PUT /api/models/{modelId}/tables/{tableId}/transforms/{transformId}”Update a transform step
400 when the new config cannot compile, an add_column expression that reads past its own row included, as on create.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes | |
transformId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
config |
object | no | |
label |
string | no | |
isEnabled |
boolean | no |
Responses: 200
DELETE /api/models/{modelId}/tables/{tableId}/transforms/{transformId}
Section titled “DELETE /api/models/{modelId}/tables/{tableId}/transforms/{transformId}”Delete a transform step
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes | |
transformId |
string | yes |
Responses: 200
POST /api/models/{modelId}/tables/{tableId}/transforms/reorder
Section titled “POST /api/models/{modelId}/tables/{tableId}/transforms/reorder”Reorder transform steps
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes | |
tableId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
orderedIds |
array of string | yes |
Responses: 200
POST /api/models/{modelId}/tables/resync
Section titled “POST /api/models/{modelId}/tables/resync”Re-sync imported tables with the source’s latest discovered schema
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Responses: 200
POST /api/models/{modelId}/validate-expression
Section titled “POST /api/models/{modelId}/validate-expression”Validate a calculated-field expression
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
modelId |
string | yes |
Request body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
expression |
string | yes | |
tableId |
string | no | When set, referenced columns are checked against this table’s schema |
Responses: 200 · 400 · 401 · 404