Möbius
Reference

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:

ValuePurpose
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 tokenReturned 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

ScenarioStatusNotes
Missing/invalid Authorization header401Only JWT access tokens are accepted; client tokens (tf_…) are rejected
Refresh token expired or reused401Refresh secrets rotate on every successful call
Table outside granted scope403Add the appropriate tables:<slug>:read|write scope
Unknown or inactive table ID404Verify the table is active and the ID matches the dashboard
Too many requests429Back 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-API token and a separate MANAGEMENT token — 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 scopeworkspace:read:audit
Role to mintADMIN 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 scopeworkspace:read:members
Role to mintADMIN 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 scopeworkspace:write:certify
Role to mintSTEWARD, 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 scopeworkspace:write:definition
Role to mintSTEWARD, 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 scopeworkspace:write:recertify
Role to mintSTEWARD, 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.json

That 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.

On this page