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"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.
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.
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.
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.
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.
| Status | code | Meaning |
|---|---|---|
| 400 | bad_request | Request body could not be parsed as JSON |
| 401 | unauthorized | Missing / unknown / revoked / expired token |
| 403 | insufficient_scope | Token lacks the required scope (required field names it) |
| 403 | forbidden_role | Approve / reject only: caller's workspace role is below approver|admin|owner |
| 403 | token_unusable | Mutation routes only: the user who minted this token was deleted - mint a new token |
| 403 | workspace_forbidden | Session-JWT caller with no access to the resolved workspace - missing or wrong x-workspace-id |
| 402 | plan_limit / email_not_confirmed / budget codes | Plan cap or billing gate refused the action |
| 422 | tier_ineligible | Approve only: commerce write-back requires Max or Enterprise plan - upgrade in Settings → Billing |
| 422 | no_commerce_integration | Approve only: no connected Shopify integration for this workspace - connect one in Settings → Integrations |
| 404 | not_found | No such resource in your workspace |
| 409 | conflict | State transition not allowed from the current state |
| 422 | validation_error | Body or query-param failed Zod schema validation (issues lists fields) |
| 429 | rate_limited | Rate limit exceeded - per token (pck_) or per user (session) - honor Retry-After |