REST API reference
Integration tokens, table endpoints, and management-plane endpoints for the Möbius REST API.
Learn how Möbius issues integration credentials, what each token is used for, and how to call the table endpoints with the right scopes. External REST, GraphQL, and MCP doors are all machine-only — they accept a service token, never a personal login session.
Two token families, two doors
A service token carries exactly one audience. DATA-API tokens (minted in
Settings → Integrations) call the table endpoints below. MANAGEMENT tokens (minted from the
same page's "Management API tokens" card) call the management endpoints. The two are disjoint —
one token, one plane. A DATA-API token presented to a management endpoint is refused with 401
before its scopes are even read, and a MANAGEMENT token cannot read or write table rows.
Token overview
Creating an integration issues three distinct values:
| Value | Purpose |
|---|---|
Client token (tf_…) | Long-lived identifier, masked in the dashboard. Not accepted by the REST API itself — use it for labeling credentials in your vault, revocation scripts, and audit trails |
| Access token (JWT) | Returned as tokens.accessToken. Send it as Authorization: Bearer <token> on every call. Contains your granted scopes and expires after 60 minutes |
| Refresh token | Returned once as tokens.refreshToken, format <tokenId>.<secret>, valid for 30 days. POST /api/integration/refresh mints a new access token and rotates the refresh secret |
Every table scope looks like tables:<table-slug>:<read|write>. Read grants
GET /api/tables/{tableId}/records; write is required for POST/PATCH/DELETE. Scopes are
embedded in the access token — the API denies requests outside that list.
Issuing a token
Authenticate with a user access token belonging to the table owner or a workspace admin.
env is a required top-level field — the single source of truth for the token's environment
scope, never derived from the selected tables.
curl -X POST "{{baseUrl}}/api/integration/tokens" \
-H "Authorization: Bearer ${USER_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "Warehouse Sync Job",
"env": "production",
"scopes": [
{ "tableId": "cmgnksxlm0gtkt6hsmcy5cam0", "permissions": ["read", "write"] }
]
}'The response includes the client token, a fresh access token, and a refresh token bundle. Store the access and refresh tokens immediately — the refresh token is never shown again.
Calling table endpoints
All table endpoints live under /api/tables/{tableId}. The table ID is the id shown in the
dashboard (not the human-friendly slug). Pass the integration access token in the bearer header.
The base URL for this door has no extra path segment beyond /api — note that the gateway
surfaces reached via a service token (REST, MCP, GraphQL) each publish their own base URL, and
the REST gateway's includes a stage segment where MCP and GraphQL do not; use the base URL shown
on the Integration page for the door you're calling.
Fetch records
curl -X GET "{{baseUrl}}/api/tables/cmgnksxlm0gtkt6hsmcy5cam0/records?page=1&limit=25" \
-H "Authorization: Bearer ${INTEGRATION_ACCESS_TOKEN}"Optional query parameters: page (1-based), limit (max 100), search for text search.
Create a record
curl -X POST "{{baseUrl}}/api/tables/cmgnksxlm0gtkt6hsmcy5cam0/records" \
-H "Authorization: Bearer ${INTEGRATION_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{ "name": "Jane Doe", "email": "jane@example.com", "status": "active" }'The payload must include all required columns. A write scope is mandatory.
Update a record
curl -X PATCH "{{baseUrl}}/api/tables/cmgnksxlm0gtkt6hsmcy5cam0/records/rec_123" \
-H "Authorization: Bearer ${INTEGRATION_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{ "status": "inactive", "last_contact": "2024-12-31T00:00:00.000Z" }'Patch only the fields that need to change.
Delete a record
curl -X DELETE "{{baseUrl}}/api/tables/cmgnksxlm0gtkt6hsmcy5cam0/records/rec_123" \
-H "Authorization: Bearer ${INTEGRATION_ACCESS_TOKEN}"Returns 404 if the record does not exist or is outside your scope.
Bulk update or insert
curl -X POST "{{baseUrl}}/api/tables/cmgnksxlm0gtkt6hsmcy5cam0/bulk-update" \
-H "Authorization: Bearer ${INTEGRATION_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"records": [
{ "id": "rec_existing", "data": { "status": "active", "plan": "Pro" } },
{ "data": { "email": "new@example.com", "status": "trial" } }
]
}'Rows with an id are updated; rows without one are created.
Upsert via import
curl -X POST "{{baseUrl}}/api/tables/cmgnksxlm0gtkt6hsmcy5cam0/import?mode=upsert&key=email" \
-H "Authorization: Bearer ${INTEGRATION_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{ "email": "alice@example.com", "status": "active" },
{ "email": "bob@example.com", "status": "inactive" }
]
}'key names the column that uniquely identifies a row; matching rows are updated, others inserted.
Rotating access tokens
Access tokens expire after 60 minutes.
curl -X POST "{{baseUrl}}/api/integration/refresh" \
-H "Content-Type: application/json" \
-d '{ "refreshToken": "${INTEGRATION_REFRESH_TOKEN}" }'- Every successful refresh returns a new access token and refresh token — discard the previous refresh token.
- An invalid, expired, or reused refresh token gets a
401.
Rate limiting
Table-endpoint calls are rate-limited 200 requests per minute. A response over the limit
returns 429 with a Retry-After header (seconds until the window resets), plus
X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset on every response so you can
throttle before you hit it.
Data-plane error reference
| Scenario | Status | Notes |
|---|---|---|
| Missing/invalid Authorization header | 401 | Only JWT access tokens are accepted; client tokens (tf_…) are rejected |
| Refresh token expired or reused | 401 | Refresh secrets rotate on every successful call |
| Table outside granted scope | 403 | Add the appropriate tables:<slug>:read|write scope |
| Unknown or inactive table ID | 404 | Verify the table is active and the ID matches the dashboard |
| Too many requests | 429 | Back off for the duration in Retry-After before retrying |
Best practices
- Store client, access, and refresh tokens in a secrets manager. Never commit them to source control.
- Rotate access tokens well before the 60-minute expiry in long-running jobs.
- Revoke unused integrations from the dashboard to invalidate their tokens immediately.
- Two tokens for unattended automation. A pipeline that both writes data and triggers a
management action (loads rows, then recertifies) should hold a
DATA-APItoken and a separateMANAGEMENTtoken — never one token spanning both. A leaked data credential then cannot perform governance actions.
Management plane endpoints
These let external systems perform management actions — read the audit log, list members, trigger certification, author governance definitions, signal a pipeline data-refresh — using a Service Token in the same bearer-JWT format as the data-plane endpoints. A management token is a separate credential from a data token, minted in Settings → Integrations ("Management API tokens" card). Read scopes require ADMIN or above to mint; write scopes require STEWARD or above. The server enforces this at mint time regardless of the UI.
GET /api/workspace/audit-log
| Required scope | workspace:read:audit |
| Role to mint | ADMIN or OWNER |
Returns workspace audit log entries in reverse-chronological order, filtered to the caller's workspace only. Also callable with a session JWT (any active workspace member).
Query parameters: limit (1–200, default 50), cursor (from the previous page's nextCursor),
eventType (exact match, e.g. TABLE_CERTIFIED), since (ISO-8601 timestamp).
curl -X GET "{{baseUrl}}/api/workspace/audit-log?limit=25&eventType=TABLE_CERTIFIED&since=2024-01-01T00:00:00.000Z" \
-H "Authorization: Bearer ${MANAGEMENT_ACCESS_TOKEN}"{
"entries": [
{
"id": "clz1234567890",
"userId": "usr_abc123",
"tableId": "cmgnksxlm0gtkt6hsmcy5cam0",
"action": "TABLE_CERTIFIED",
"resource": "semantic_contract",
"environmentLabel": "production",
"gateModeAtEvaluation": "ENFORCING",
"createdAt": "2024-12-31T10:00:00.000Z"
}
],
"nextCursor": "clz9876543210"
}Errors: 401 missing/invalid token or wrong audience · 403 missing scope · 400 invalid query
parameters.
GET /api/workspace/members
| Required scope | workspace:read:members |
| Role to mint | ADMIN or OWNER |
Returns all active workspace members with role and environment scope. Platform operators are excluded. Same response shape for Service Token and session-JWT callers.
curl -X GET "{{baseUrl}}/api/workspace/members" \
-H "Authorization: Bearer ${MANAGEMENT_ACCESS_TOKEN}"{
"members": [
{ "userId": "usr_abc123", "email": "alice@example.com", "role": "ADMIN", "envScope": ["dev", "staging"] }
]
}Errors: 401 missing/invalid token or wrong audience · 403 missing scope.
POST /api/tables/[id]/semantic-contract/certify
| Required scope | workspace:write:certify |
| Role to mint | STEWARD, ADMIN, or OWNER |
Triggers certification of a table's semantic contract. The table must be in PENDING_REVIEW with
a valid, fresh review proof. Unlike the other management endpoints, a session caller needs
STEWARD+ workspace role, or a Service Token needs write permission on the target table and the
workspace:write:certify scope — in practice only a MANAGEMENT token carries that scope, since
DATA-API tokens are refused it at mint.
Request body (all fields optional): note (free-text, recorded in the audit log),
evidenceFileName, evidenceHash (SHA-256), evidenceSize.
curl -X POST "{{baseUrl}}/api/tables/cmgnksxlm0gtkt6hsmcy5cam0/semantic-contract/certify" \
-H "Authorization: Bearer ${MANAGEMENT_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{ "note": "Certified by CI pipeline after passing all predicates." }'{
"contract": {
"id": "sc_xyz789",
"status": "CERTIFIED",
"certifiedAt": "2024-12-31T10:05:00.000Z",
"grain": "one row per customer",
"grainType": "ONE_PER_ENTITY",
"grainKeyColumns": ["customer_id"]
}
}grainType / grainKeyColumns are an optional structured grain declaration alongside the
free-text grain field — never a substitute for it. grainType: null means no structured
declaration exists.
Errors: 401 no valid credential · 403 missing scope, table write permission, or role below
STEWARD · 404 table not found · 400 table not in PENDING_REVIEW · 422 certification blocked
(stale proof, failed predicates, Gate 3 required, or inspection policy not satisfied).
POST /api/workspace/definitions
| Required scope | workspace:write:definition |
| Role to mint | STEWARD, ADMIN, or OWNER |
Registers or replaces a governance definition artifact (SQL/YAML/JSON) — the management-token
equivalent of mb push. Management-token-only — there is no session-JWT branch and no CLI
wrapper; a person pushes via the dashboard UI or mb push instead. Creating a new artifact returns
202 with the auto-enqueued baseline gate-run job; replacing an existing one (replaceId)
reconciles columns, resets the table to QUARANTINED, and triggers recertification if the table
was certified.
Request body: filename (required — also the external source reference), content (required),
mimeType (required — sql, yaml, or json), connectorId (required — the artifact's Mode A
tables inherit the connector's environment), replaceId (optional), source (optional explicit
schema.table address; falls back to the artifact's own source reference).
curl -X POST "{{baseUrl}}/api/workspace/definitions" \
-H "Authorization: Bearer ${MANAGEMENT_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"filename": "customer_risk_profile.sql",
"content": "CREATE OR REPLACE VIEW customer_risk_profile AS SELECT ...",
"mimeType": "sql",
"connectorId": "conn_abc123"
}'{
"governanceId": "cm_gov_abc123",
"trackingUrl": "https://your-mobius-url.com/tables/cm_gov_abc123",
"job": { "id": "job_bb01", "type": "gate-baseline-auto", "status": "QUEUED" }
}Errors: 401 wrong audience · 403 missing scope or refused connector/table environment ·
404 connector not found, or replaceId mismatch · 409 an artifact with the same name or
source already exists (use replaceId) · 400 missing/invalid fields or an unresolvable source.
POST /api/workspace/recertify — pipeline signal
| Required scope | workspace:write:recertify |
| Role to mint | STEWARD, ADMIN, or OWNER |
The event-driven signal a transformation pipeline (dbt / a stored procedure / an orchestrator /
CI) calls from its post-run step to tell Möbius "I just rewrote these tables," so certification is
re-checked instead of waiting for the next access-driven check. Management-token-only,
deliberately a narrower scope than workspace:write:certify — a leaked pipeline credential must
not carry full certification authority. Submissions that don't match a governed Mode A table
silently no-op (outcome: not_governed) rather than erroring, so a large dbt run doesn't need to
pre-filter itself.
Environment resolution: if the token was minted with a single environment, that environment is
used and a body environmentLabel is optional but must match if present (else 403). If the token
is environment-agnostic (the default), environmentLabel is required in the body. Mint
pipeline tokens environment-scoped so a dev credential can't signal prod.
Request body: tables (array of 1–500 names, or { name, schema?, definitionHash? } objects —
matched case-insensitively) or runResults (the parsed contents of dbt's
run_results.json — Möbius derives the rebuilt-table batch and evidence summary from it),
exactly one of the two is required. manifest (optional, alongside runResults) resolves
relation names for semantic-drift detection; a bad manifest degrades gracefully, it never fails the
call. trigger defaults to "PIPELINE_RUN". evidence is an optional cheap-tier summary
stamped on the audit event — never substituted for certification predicates.
curl -X POST "{{baseUrl}}/api/workspace/recertify" \
-H "Authorization: Bearer ${MANAGEMENT_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"tables": ["customer_risk_profile", "orders"],
"environmentLabel": "production",
"trigger": "PIPELINE_RUN",
"evidence": { "kind": "dbt", "runId": "run_20260725_01", "summary": { "modelsRebuilt": 2, "testsPassed": 14, "testsFailed": 0 } }
}'A body that is itself a raw run_results.json (top-level metadata object + results array, no
tables/runResults key) is recognized by shape and handled the same way — post dbt's artifact
unmodified:
dbt run --target prod && \
curl -X POST "{{baseUrl}}/api/workspace/recertify" \
-H "Authorization: Bearer ${MANAGEMENT_ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
--data-binary @target/run_results.jsonThat shortcut form carries no environmentLabel and no manifest, so it requires an
environment-scoped token.
{
"environmentLabel": "production",
"submitted": 2,
"governed": 1,
"outcomes": [
{ "table": "customer_risk_profile", "outcome": "recert_filed", "driftDetected": false },
{ "table": "orders", "outcome": "not_governed", "driftDetected": false }
]
}Errors: 401 wrong audience · 403 missing scope, or a payload environmentLabel mismatching an
environment-scoped token · 400 missing environmentLabel for an environment-agnostic token, an
unconfigured environment tier, an invalid tables array, neither/both of tables/runResults, a
malformed run_results, a run that built nothing, or a batch above 500 tables · 413 request body
over 25 MB.
Pipeline test results are context only — Möbius always verifies certification independently; dbt evidence corroborates the audit trail, it never substitutes for a predicate.