StartConceptsPlaybooksFeature docsAPI & MCPChangelog

API & MCP

Your account is an MCP server. Issue a Bearer key at Settings → API Keys, point Claude Cowork (or any MCP-compatible agent) at /api/v1/mcp, and it can call the tools below against your account. Every tool is also a plain REST endpoint if you'd rather not speak MCP.

Authentication

Issue a key at /settings/api-keys. Keys are Bearer-style:

Authorization: Bearer sak_abc1…xyz

Two ways to call

Same tools, same keys, same scopes, same audit log — pick the transport that fits your client.

MCP (JSON-RPC)

Point an MCP client at https://salesascode.com/api/v1/mcp. It calls tools/list to introspect, then tools/call to invoke. This is what the Sales as Code Cowork plugins use.

REST

POST JSON to /api/v1/tools/<name> with a Bearer header:

curl -X POST https://salesascode.com/api/v1/tools/log_capture \
  -H 'authorization: Bearer sak_…' \
  -H 'content-type: application/json' \
  -d '{"source":"cowork","text":"Acme champion confirmed Q3 budget"}'

stdio (local bridge)

HTTP is the canonical transport. For MCP clients that only speak stdio (Claude Desktop, some Cowork setups), run the bundled bridge — a thin local proxy that forwards newline-delimited JSON-RPC to the same /api/v1/mcp endpoint with your Bearer key. Same tools, same scopes, same audit log; no server credentials required beyond the key.

From the repo: npm run mcp:stdio (reads SALES_AS_CODE_API_KEY and optional SALES_AS_CODE_URL from the environment). To wire it into Claude Desktop, add a server to claude_desktop_config.json:

{
  "mcpServers": {
    "sales-as-code": {
      "command": "node",
      "args": ["/abs/path/to/scripts/mcp-stdio.mjs"],
      "env": {
        "SALES_AS_CODE_API_KEY": "sak_…",
        "SALES_AS_CODE_URL": "https://salesascode.com"
      }
    }
  }
}

The bridge holds no business logic — it relays stdio ⇆ HTTP. Auth, scopes, rate limits, and the audit log all live on the server, exactly as with a direct HTTP client.

Per-user and per-team keys

Every key belongs to the user who issues it. A team owner can instead issue a team key (a toggle in the New key dialog): it still writes into the owner's workspace, but the control-plane tools resolve entitlements against the team's plan — the union of the owner's grants and every plugin the team's seat packs license. A per-user key is unchanged; it authorizes against that user's own plan. verify_session reports scope: "team" or "user" and, for team keys, the team id.

OAuth-provider mode (optional)

Instead of pasting a key, an MCP client can obtain access through a standard OAuth 2.0 authorization-code + PKCE flow. This is env-gated — off unless the deployment sets MCP_OAUTH_ENABLED=true; when off, the endpoints below return 404 and pasted Bearer keys are the only path.

The honest trick that keeps it small: the OAuth access token is a managed API key. The token endpoint mints a scoped api_keys row under the hood, so an OAuth-obtained token revokes, audits, and rate-limits exactly like a hand-issued key — you'll see it in Settings → API keys named OAuth (<client>).

Honest scope: PKCE (S256) is required; there is no dynamic client registration (clients pass an opaque client_id; the code binds the redirect_uri) and no refresh tokens (the minted key doesn't expire — revoke it in Settings). Codes are single-use and expire in 10 minutes.

The tools

Sixteen tools in three groups.

Data verbs (scoped)

Thin facades over the same store the app uses, so external writes and in-app data converge on one record shape. Each needs the scope shown.

Control-plane tools

The bridge every Cowork plugin uses: verify the licensed session and gate on entitlement first, then report telemetry and brain state last. No extra scope — a valid key on its own account is enough.

Onboarding stays open; the rest need entitlement. A brand-new account with a key but no purchase can always call verify_session, check_entitlement, and pull_config — that's how a plugin connects and discovers it needs to buy. The telemetry and brain tools in this group (start_run, run_heartbeat, emit_telemetry, sync_brain) write your brain/run state, so they require an active entitlement just like the data verbs and return not_entitled otherwise.

Content-engine tools

Used by the Content Engine plugin to connect channels and drive the studio from Cowork. Also no extra scope.

Telemetry lifecycle for plugin skills

What a well-behaved skill emits, in order. Everything here is optional beyond the final emit_telemetry — old skills keep working — but the more of it a skill sends, the better the rollup gets (live "Running now" strip, honest durations, exact run ↔ brain correlation).

  1. Start. Call start_run (after verify_session / check_entitlement) with the plugin + skill. Keep the returned run_ref for the rest of the run. The run appears on the rollup as running immediately.
  2. Heartbeat. For runs longer than a few minutes, call run_heartbeat with the run_refat most once every 60 seconds (cadence floor; heartbeats multiply rows of noise if spammed, and once per minute is all the staleness rule needs). A started run with no heartbeat for 30 minutes is flagged stale on the dashboard, so a crashed skill never looks "running" forever.
  3. Brain syncs. Pass the same run_ref on every sync_brain call the run makes. That upgrades the run drill-down from "syncs that landed around this run" (time-window guess) to exact attribution.
  4. Final. End with one emit_telemetry carrying the run_ref as run_id, plus counts, outcome, and the error taxonomy on failure. This settles the declared run (runningok/error) in place — one run, one row. Omit duration_ms and it is computed from the declared start.

Report silent work too. If a Cowork session did exploratory or advisory work that never triggered a discrete skill (research, Q&A, drafting that wasn't saved), emit a session summary before the session ends: emit_telemetry with skill: "session-summary" and honest counts (e.g. {"questions_answered": 4, "accounts_researched": 2}). Otherwise that work is invisible on the rollup and the user can't see what their plugins actually did for them.

start_run        {"plugin":"gtm-engine","skill":"daily-signals"}      → {"run_ref":"run_kJ2…"}
run_heartbeat    {"run_ref":"run_kJ2…"}                               (every ≥60s while working)
sync_brain       {"scope":"signal-weights","payload":{…},"run_ref":"run_kJ2…"}
emit_telemetry   {"plugin":"gtm-engine","skill":"daily-signals",
                  "run_id":"run_kJ2…","outcome":"ok","counts":{"signals_sensed":12}}

Account scoping — account_ref

Both emit_telemetry and sync_brain accept an optional account_ref: a caller-supplied string label for the account a run worked or a brain snapshot is about — a domain ("acme.com") or an account name ("Acme Corp"). It is capped at 200 characters and otherwise unvalidated — an honest free-text label, not a foreign key and never synced CRM records or contact PII. It preserves the metadata-only trust posture: the same label the seller already sees, nothing more.

Where it surfaces: the per-account filter on the personal rollup scopes the run history + by-plugin summary to one account; the team rollup shows per-account, per-member activity and a "heads up — teammates worked the same account this week" deconfliction card. Both are metadata-only reporting over these labels — no account data is ever synced to render them.

Error envelope

Non-2xx responses carry a consistent JSON shape:

{ "error": "unauthorized", "message": "Key is missing or revoked." }

Status codes: 200 ok · 400 invalid input · 401 missing/revoked key · 403 key lacks the required scope or the account has no active entitlement (not_entitled — buy at /pricing) · 404 unknown tool / resource · 429 rate-limited · 500 server error. Over MCP JSON-RPC the entitlement denial is error code -32003.

Audit & limits

Every call — MCP or REST — writes to your audit log, visible to the issuing user. Keys are rate-limited per key; a misbehaving client on one key can't 429 another. See the API Keys & MCP feature page for the issuing flow and the security model, and Concepts → the MCP contract for how the plugins use these tools.