Quickstart

Mint a pck_… token at Settings → API keys, then set TOKEN=pck_… and run any of the examples below. Replace the production URL with http://localhost:3000 for local development.

# List recent runs (scope: runs:read)
curl -s https://www.margintide.com/api/v1/runs \
  -H "Authorization: Bearer $TOKEN"

# Trigger a run (scope: runs:trigger)
curl -s -X POST https://www.margintide.com/api/v1/runs \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"subset_tags": ["core-skus"]}'

# List recommendations (scope: recommendations:read)
curl -s "https://www.margintide.com/api/v1/recommendations?status=new" \
  -H "Authorization: Bearer $TOKEN"

# Accept a recommendation (scope: recommendations:write)
curl -s -X POST https://www.margintide.com/api/v1/recommendations/$REC_ID/accept \
  -H "Authorization: Bearer $TOKEN"

# Approve a pending_approval recommendation - triggers Shopify write-back
# (scope: recommendations:approve - Max / Enterprise plan required)
curl -s -X POST https://www.margintide.com/api/v1/recommendations/$REC_ID/approve \
  -H "Authorization: Bearer $TOKEN"

# Reject a pending_approval recommendation with a reason
# (scope: recommendations:approve)
curl -s -X POST https://www.margintide.com/api/v1/recommendations/$REC_ID/reject \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reason": "price too aggressive for Q3 margin target"}'

# Snooze an alert (scope: alerts:write)
curl -s -X POST https://www.margintide.com/api/v1/alerts/$ALERT_ID/snooze \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"snooze_until": "2026-07-01T00:00:00Z", "reason": "supplier renegotiation"}'

# List catalog items (scope: catalog:read)
curl -s https://www.margintide.com/api/v1/catalog/items \
  -H "Authorization: Bearer $TOKEN"

# Export report data (scope: reports:read)
curl -s https://www.margintide.com/api/v1/reports/export \
  -H "Authorization: Bearer $TOKEN"

Authentication & workspaces

Two supported credentials: Authorization: Bearer pck_… (an API token — primary; works for every /api/v1/* route), or a Supabase session JWT (first-party apps only — mobile approve/reject, workspace-status). Session callers with more than one workspace membership must send x-workspace-id; omitting it returns 403 workspace_forbidden.

One API token belongs to exactly one workspace, forever. A caller with N workspaces creates N tokens (one per workspace) to reach all of them — there is no cross-workspace token.

Rate limits

Default 60 requests/minute, budgeted independently per pck_ token, per session user, and per OAuth grant. A pck_ token’s budget is shared between this REST API and the MCP server — the same requests count against both. Exceeding the budget returns 429 rate_limited with a Retry-After header (seconds until the window resets). There is no X-RateLimit-* header family.

Versioning & deprecation

Within /api/v1 only additive changes ship: new endpoints, new optional request fields, new response fields, new enum values. Clients must ignore unknown fields and enum values. Breaking changes ship under a new path prefix (/api/v2), never as a mutation of v1. A deprecated v1 operation is marked deprecated: true in the OpenAPI spec, listed here, and its workspace owners/admins are emailed — the operation keeps working for at least 6 months after that notice before removal.

Deprecated operations: none.

Workspace-status trust fields

GET /api/v1/workspace/status (scope: workspace:read) also returns two read-only transparency projections alongside lifecycle and usage — both are required response keys that are null when the underlying state is absent, never omitted.

support_access is non-null only while a platform support session is actively open against the caller’s own workspace — the owner-transparency view onto the consent-gated support-access flow. It carries only active, admin_email, started_at, and expires_at — never a session id, consent code, encrypted token, or any other workspace’s data.

account_deletion is non-null only while the calling user has an active (pending or blocked) account-deletion request, scoped strictly to that caller. It carries scheduled and effective_at — the purge-not-before date — so a client can surface a deletion-pending banner without a second request.

Prefer MCP access? The MCP server guide covers connecting Claude Desktop or a custom MCP client directly - no curl, no dashboard. Requires a Max or Enterprise plan.

Error cheat-sheet

Re-sending an accept / dismiss / snooze / resolve / approve that already took effect returns 200 { "ok": true, "idempotent": true } - safe to retry on network failures. The approve endpoint uses CAS (compare-and-swap) for exactly-once Shopify store writes.

StatuscodeMeaning
400bad_requestRequest body could not be parsed as JSON
401unauthorizedMissing / unknown / revoked / expired token
403insufficient_scopeToken lacks the required scope (required field names it)
403forbidden_roleApprove / reject only: caller's workspace role is below approver|admin|owner
403token_unusableMutation routes only: the user who minted this token was deleted - mint a new token
403workspace_forbiddenSession-JWT caller with no access to the resolved workspace - missing or wrong x-workspace-id
402plan_limit / email_not_confirmed / budget codesPlan cap or billing gate refused the action
422tier_ineligibleApprove only: commerce write-back requires Max or Enterprise plan - upgrade in Settings → Billing
422no_commerce_integrationApprove only: no connected Shopify integration for this workspace - connect one in Settings → Integrations
404not_foundNo such resource in your workspace
409conflictState transition not allowed from the current state
422validation_errorBody or query-param failed Zod schema validation (issues lists fields)
429rate_limitedRate limit exceeded - per token (pck_) or per user (session) - honor Retry-After