Skip to content

MOS API — external builder quickstart

Build a "vibe app" that sits adjacent to MOS: your app holds a MOS team token and calls the API; MOS is the system of record, your app is a UX shell. You never touch the database — the token's team_id + RLS are the tenant boundary, so your app can only ever see the one team it was issued for.

The full surface map (what's exposed, what's a gap) is in agentic-rails.md. This is the 5-minute on-ramp.


1. Mint a token

UI: sign in to your team app (https://<your-team>.marduk.app) → Admin → Settings → API Keys (/admin/settings/api-keys) → New token. Copy the jst_… value once — it is shown only at creation and stored hashed.

Token types

Type Expiry Trust (on /api/v1/mcp/actions) Use for
service none (rotate manually) trusted — mutations execute immediately server-to-server integrations, your adjacent app's backend
human 1h access + 30d refresh untrusted — mutations queue for human approval a user acting through your app
break_glass 4h untrusted audited emergency access

Scopes. A token carries a JSONB scope list ([{resource, actions[]}]). Leaving it empty grants full team access (the wildcard *). Every v1 resource route declares its true resource (G-1 closed, mos-hq6q4.81), so a least-privilege token works — e.g. forms:read can list forms but gets 403 on tables. Scope resources include contacts, tables, forms, files, artifacts, bases, review, workflows, wellness, extension, vault, comms, emr, webhooks, audit, tokens, *; actions are read / write / delete / admin. The MCP dispatch surface still requires the wildcard — see agentic-rails.md §G-1.

Every request is Authorization: Bearer jst_….


2. The first three calls

Base URL is your team host, e.g. https://acme.marduk.app. (The versioned API is served from every team subdomain.)

a) List tables

curl -s https://acme.marduk.app/api/v1/data/tables \
  -H "Authorization: Bearer $MOS_TOKEN"
# → { "tables": [ { "id": "...", "name": "...", "display_name": "..." }, ... ] }

/api/v1/data/tables is the canonical path (what the /api/v1 index advertises). The older /api/v1/tables/tables spelling still works as an alias — both run the same handlers.

b) Create a row

curl -s -X POST https://acme.marduk.app/api/v1/data/tables/$TABLE_ID/rows \
  -H "Authorization: Bearer $MOS_TOKEN" -H "Content-Type: application/json" \
  -d '{ "data": { "name": "Widget", "qty": 3 } }'

c) Subscribe to changes — register a webhook endpoint and MOS pushes domain events to you (G-16, closed by mos-hq6q4.78):

curl -s -X POST https://acme.marduk.app/api/v1/webhooks \
  -H "Authorization: Bearer $MOS_TOKEN" -H "Content-Type: application/json" \
  -d '{ "url": "https://yourapp.example/hooks/mos", "events": ["contact.created", "invoice.paid"] }'
# → 201 { "id": "…", "url": "…", "events": ["…"], "secret": "…" }  ← secret shown ONCE

Every matching event is POSTed to your URL, HMAC-SHA256-signed so you can verify it: X-Webhook-Signature: sha256=<hex digest of the raw body using your secret>. Failed deliveries retry with exponential backoff (1 → 2 → 4 → 8 → 16 min) before giving up. Manage your subscriptions and inspect the last 25 deliveries per endpoint through the same surface:

curl -s https://acme.marduk.app/api/v1/webhooks -H "Authorization: Bearer $MOS_TOKEN"      # list + delivery stats
curl -s https://acme.marduk.app/api/v1/webhooks?events=1 -H "Authorization: Bearer $MOS_TOKEN"  # subscribable event types
curl -s https://acme.marduk.app/api/v1/webhooks/$ID -H "Authorization: Bearer $MOS_TOKEN"  # detail + recent deliveries

Requires the webhooks:read / webhooks:write (and webhooks:delete to remove) scopes — a wildcard token has them all. Polling is still fine for low-volume use: GET .../rows?sort=updated_at:desc, tracking the newest updated_at you've seen.

Discover the whole surface at runtime:

curl -s https://acme.marduk.app/api/.well-known/openapi.json   # OpenAPI 3.1, no auth
curl -s https://acme.marduk.app/api/v1 -H "Authorization: Bearer $MOS_TOKEN"

3. MCP config (Claude / Cursor / any MCP client)

MOS speaks MCP over plain HTTP (JSON-RPC 2.0) at /api/v1/mcp — 10 tools: list_tables, query_rows, insert_row, list_forms, list_bases, list_workflows, create_invoice, create_quote, create_checkout, create_subscription.

{
  "mcpServers": {
    "mos": {
      "type": "http",
      "url": "https://acme.marduk.app/api/v1/mcp",
      "headers": { "Authorization": "Bearer jst_YOUR_TOKEN_HERE" }
    }
  }
}

The #1 gotcha: an empty token → 401 {code:-32000}. If you template the token from an env var ("Bearer ${MOS_MCP_TOKEN}") and the var is unset, the client sends Bearer with no value and every call 401s. The server now tells you exactly which case you hit ("empty Bearer token…", "not a MOS team API key…", "invalid, revoked, or expired"). Sanity-check with curl before wiring a client:

curl -s -X POST https://acme.marduk.app/api/v1/mcp \
  -H "Authorization: Bearer $MOS_MCP_TOKEN" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
# → {"jsonrpc":"2.0","id":1,"result":{"serverInfo":{"name":"jst-client-api",...}}}

The URL must include /api/v1/mcp. A bare host (https://acme.marduk.app) gives the client's own ENDPOINT_NOT_FOUND.

There is a second, richer MCP server at /api/v1/mcp/actions that exposes every MOS action — but it is off by default (per-tenant modules.mcp_export) and returns 403 until enabled. See agentic-rails.md.

For a person connecting ChatGPT, Claude.ai, Claude Code, Codex or pi, prefer the OAuth door over a pasted token: the same URL, discovered via /.well-known/oauth-protected-resource, with a scoped, expiring, revocable consent grant. See agentic-rails § "Connecting an outside agent" and the per-harness guides in docs/agent-door/.


4. Rate limits

Per token, sliding window, fail-open (a flaky limiter never takes the API down):

Token type Limit
service 10 000 / min
human, break_glass 1 000 / min

Over the limit → 429 with a Retry-After header (seconds). Back off and retry.


5. What "adjacent app" means

  • Your app holds the token. Store it server-side; never ship a service token to a browser. For end-user flows, mint short-lived human tokens.
  • MOS is the system of record. Don't mirror-and-diverge; read and write through the API so RLS, audit logging, and approval gates stay authoritative.
  • The team boundary is enforced for you. A token is bound to one team_id; every query is scoped to it. You cannot read another team's data even by id.
  • Mutations may be gated. On the actions MCP surface, human/break_glass tokens propose changes for human review rather than applying them — design your UX to show "pending approval" states.

Questions or a gap that blocks you? The ranked gap list (contacts, files, …) is in agentic-rails.md; each is a tracked task under mos-hq6q4.

Last verified: 2026-09-06 (task mos-hq6q4.78 — G-16 webhook subscriptions live).


© 2026 MOS Labs LLC. All rights reserved.