{"openapi":"3.1.0","info":{"title":"Prufa public API","version":"1.0.0","description":"Robust QA software, built for the agentic era. The LLM navigates, the code verifies. The CLI, the HTTP API, and the MCP server are three views of the same product. See /docs for the design system."},"servers":[{"url":"/api/v1","description":"versioned v1 surface"}],"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","bearerFormat":"API key"},"deployToken":{"type":"apiKey","in":"header","name":"X-Prufa-Token","description":"Per-monitor deploy-hook secret (token-auth easy mode). Shown once at monitor creation; rotatable via POST /monitors/{monitor_id}/webhook/rotate."}},"schemas":{"CreateAuditRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Public, http(s) URL only"},"source":{"type":["string","null"],"maxLength":32,"description":"Optional caller marker (e.g. 'landing') — logged for funnel counting, no behavior change"}}},"AuditCreated":{"type":"object","required":["run_id","status","events_url","report_url"],"properties":{"run_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","succeeded","failed","blocked","timeout"]},"events_url":{"type":"string"},"report_url":{"type":"string"}}},"Run":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string"},"status":{"type":"string"},"failure_reason":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"},"report_url":{"type":"string"}}},"Finding":{"type":"object","properties":{"check_id":{"type":"string"},"tier":{"type":"string","enum":["verified","advisory"]},"severity":{"type":"string","enum":["critical","warning","info"]},"title":{"type":"string"},"impact":{"type":"string"},"evidence":{"type":"object","additionalProperties":true}}},"Error":{"type":"object","required":["code","hint"],"properties":{"code":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"}}}}},"paths":{"/audits":{"post":{"summary":"Create a one-shot audit","operationId":"create_audit","security":[],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAuditRequest"}}}},"responses":{"202":{"description":"Audit queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuditCreated"}}}},"422":{"description":"URL guard rejected (private target, bad scheme)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"List recent runs in this workspace","operationId":"list_audits","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":10,"maximum":100}}],"responses":{"200":{"description":"List of runs","content":{"application/json":{"schema":{"type":"object","properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/Run"}}}}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/audits/{run_id}":{"get":{"summary":"Get a run by ID","operationId":"get_audit","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Run"},"404":{"description":"Not found"}}}},"/audits/{run_id}/report.json":{"get":{"summary":"Get the JSON report payload for a run","operationId":"get_audit_report_json","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"JSON report"}}}},"/audits/{run_id}/report-subscription":{"post":{"summary":"Email the report + schedule a one-time 7-day re-check with a diff","operationId":"create_report_subscription","description":"Anonymous, transactional: exactly two emails (the report now, the delta in 7 days), never a list. One subscription per run; per-email weekly cap; rate-limited per IP.","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"}}}}}},"responses":{"201":{"description":"Scheduled"},"200":{"description":"Already scheduled / capped (no enumeration)"},"422":{"description":"Invalid email"},"429":{"description":"Rate limited"}}}},"/report-subscriptions/unsubscribe":{"get":{"summary":"Cancel a scheduled report re-check (idempotent)","operationId":"unsubscribe_report_subscription","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cancelled"},"404":{"description":"Unknown token"}}}},"/monitors":{"post":{"summary":"Create a 1-click monitor (paid — card required)","operationId":"create_monitor","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string"},"cadence":{"type":"string","enum":["daily","hourly"],"default":"daily"},"flow_id":{"type":["string","null"],"format":"uuid","description":"Re-run this confirmed flow instead of the plain audit"}}}}}},"responses":{"201":{"description":"Monitor created — first run is immediate; response includes the deploy_hook {url, header, secret} (secret shown ONCE) and the inline usage object"},"402":{"description":"Card required / workspace billing-paused","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"flow_id not found in this workspace"},"409":{"description":"flow_not_confirmed — confirm the flow first"}}},"get":{"summary":"List workspace monitors (+ inline usage)","operationId":"list_monitors","responses":{"200":{"description":"monitors: [{id, url, cadence, status, flow_id, last_run_at, last_run_status, next_run_at, consecutive_failures}] + usage"},"401":{"description":"Missing or invalid API key"}}}},"/monitors/{monitor_id}":{"get":{"summary":"Monitor detail (+ recent_runs, deploy hook url, never_fired)","operationId":"get_monitor","parameters":[{"name":"monitor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Summary fields + recent_runs (last 10) + deploy_hook {url, header, never_fired} — the secret is NOT returned here (creation/rotation only)"},"404":{"description":"Not found"}}},"patch":{"summary":"Pause/resume a monitor or change its cadence","operationId":"patch_monitor","parameters":[{"name":"monitor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["active","paused"]},"cadence":{"type":"string","enum":["daily","hourly"]}}}}}},"responses":{"200":{"description":"Updated detail. Resuming (paused -> active) sets next_run_at = now"},"422":{"description":"invalid_status / invalid_cadence"}}},"delete":{"summary":"Delete a monitor (run/alert history is kept, links nullified)","operationId":"delete_monitor","parameters":[{"name":"monitor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}}}},"/monitors/{monitor_id}/run":{"post":{"summary":"Run now (rate-capped 1/60s per monitor)","operationId":"run_monitor_now","parameters":[{"name":"monitor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"responses":{"202":{"description":"{run_id, status, deduped, events_url, report_url, usage} — an existing queued/running run is returned with deduped: true"},"402":{"description":"quota_exceeded — billing next step in hint"},"429":{"description":"rate_limited — retry in up to 60s"}}}},"/monitors/{monitor_id}/deliveries":{"get":{"summary":"Deploy-hook delivery log (last 50) + copy-paste CI snippets","operationId":"list_monitor_deliveries","parameters":[{"name":"monitor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{deliveries: [{status, reason, created_at, run_id, requester_ip}], never_fired, snippets: {curl, github_actions, gitlab_ci}}"}}}},"/monitors/{monitor_id}/webhook/rotate":{"post":{"summary":"Rotate the deploy-hook secret (returned ONCE)","operationId":"rotate_monitor_webhook_secret","parameters":[{"name":"monitor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"{monitor_id, webhook_secret, header} — old token dead"}}}},"/hooks/deploy/{monitor_id}":{"post":{"summary":"Deploy-trigger webhook (token auth — X-Prufa-Token, no bearer)","operationId":"deploy_hook","security":[{"deployToken":[]}],"parameters":[{"name":"monitor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"description":"Payload schema v1 (DX5) — empty body is VALID","content":{"application/json":{"schema":{"type":"object","properties":{"sha":{"type":["string","null"]},"ref":{"type":["string","null"]},"env":{"type":["string","null"]},"url_override":{"type":["string","null"],"description":"Plain monitors only; must stay on the monitor's host (or a subdomain)"}}}}}},"responses":{"202":{"description":"{run_ids, results_url, deduped} — a queued run for the monitor coalesces (deduped: true). Every request writes a delivery row."},"401":{"description":"bad_token (machine-readable; delivery row written)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"quota_exceeded"},"404":{"description":"unknown_monitor"},"409":{"description":"monitor_paused / flow_not_confirmed"},"422":{"description":"invalid_payload (incl. url_override host mismatch)"},"429":{"description":"rate_limited (bad-token flood guard, 10/min/IP)"}}}},"/workspaces":{"post":{"summary":"Create a workspace (returns API token ONCE)","operationId":"create_workspace","security":[],"parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner_email"],"properties":{"owner_email":{"type":"string","format":"email"},"display_name":{"type":"string"},"tier":{"type":"string","enum":["free","agent_temp","starter","pro","team"],"default":"free"}}}}}},"responses":{"201":{"description":"Workspace created (api_token returned once)"},"422":{"description":"Invalid tier"}}}},"/workspaces/current":{"get":{"summary":"Get the current workspace (auth required)","operationId":"get_current_workspace","responses":{"200":{"description":"Workspace + inlined usage"},"401":{"description":"Missing or invalid bearer token"}}}},"/workspaces/current/usage":{"get":{"summary":"Sync usage object (anonymous or auth'd)","operationId":"get_workspace_usage","security":[],"responses":{"200":{"description":"Usage shape: calls_used, calls_included, calls_remaining, period_start/end, monitor_paused_at, opt_in_to_overage, prepaid_credit_balance"}}},"patch":{"summary":"Workspace settings: usage webhook, overage opt-in","operationId":"patch_workspace_settings","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Replays within 24h return the original response without side effects."}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"display_name":{"type":["string","null"]},"usage_webhook_url":{"type":["string","null"]},"usage_webhook_secret":{"type":["string","null"]},"opt_in_to_overage":{"type":["boolean","null"]},"email_alerts_enabled":{"type":["boolean","null"]},"slack_alerts_enabled":{"type":["boolean","null"]}}}}}},"responses":{"200":{"description":"Updated settings + usage"},"402":{"description":"tier_required — overage opt-in needs a paid tier"},"422":{"description":"invalid_webhook_url"}}}},"/workspaces/current/usage-webhook/test":{"post":{"summary":"Send a signed test event to the configured usage webhook","operationId":"test_usage_webhook","responses":{"202":{"description":"Test event sent"},"409":{"description":"webhook_unconfigured"}}}},"/integrations/slack/install":{"get":{"summary":"Start the 'Add to Slack' OAuth flow (browser redirect)","operationId":"slack_install","responses":{"302":{"description":"Redirect to Slack's consent screen"},"503":{"description":"slack_unconfigured — Slack OAuth app credentials are missing"}}}},"/integrations/slack/callback":{"get":{"summary":"Slack OAuth redirect target (one-time state; not called directly)","operationId":"slack_callback","security":[],"responses":{"302":{"description":"Redirect to the dashboard with ?slack=connected|denied|error"}}}},"/flows":{"post":{"summary":"Create a flow: NL test case compiled to a REVIEWABLE draft spec (or upload a flow-spec v1 object). Only confirmed flows run.","operationId":"create_flow","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Replays within 24h return the original response without side effects."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"},"name":{"type":["string","null"]},"test_case":{"type":["string","null"],"description":"Constrained-vocabulary NL — see /docs/guides/flow_vocabulary.html"},"spec":{"type":["object","null"],"description":"flow-spec v1 — see /docs/specs/flow_spec.schema.json"}}}}}},"responses":{"201":{"description":"Draft flow (spec + variables + review hint)"},"402":{"description":"quota_exceeded (hard cap)"},"409":{"description":"name_taken"},"422":{"description":"compile_failed | invalid_spec | unsafe_url"}}},"get":{"summary":"List flows in this workspace","operationId":"list_flows","responses":{"200":{"description":"flows: [{flow_id, name, status, ...}]"}}}},"/flows/{flow_id}":{"get":{"summary":"Get a flow (spec, variables, stored credential NAMES only)","operationId":"get_flow","responses":{"200":{"description":"Flow detail"},"404":{"description":""}}},"put":{"summary":"Edit the spec (returns the flow to draft — re-confirm to run)","operationId":"update_flow","responses":{"200":{"description":"Draft flow"}}},"delete":{"summary":"Delete a flow (runs keep their spec snapshots)","operationId":"delete_flow","responses":{"204":{"description":"Deleted"},"409":{"description":"flow_in_use — a monitor runs this flow"}}}},"/flows/{flow_id}/confirm":{"post":{"summary":"Confirm the reviewed spec (only confirmed flows run)","operationId":"confirm_flow","responses":{"200":{"description":"Confirmed flow"}}}},"/flows/{flow_id}/credentials":{"put":{"summary":"Store {{VAR}} values (encrypted at rest; write-only — values are never returned and never enter LLM context)","operationId":"put_flow_credentials","responses":{"200":{"description":"stored: [names]"},"503":{"description":"credentials_unavailable (encryption key unset)"}}}},"/flows/{flow_id}/run":{"post":{"summary":"Execute a confirmed flow (202; optional per-run credentials, never stored)","operationId":"run_flow","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Replays within 24h return the original response without side effects."}],"responses":{"202":{"description":"run_id + events_url + report_url + usage"},"402":{"description":"quota_exceeded (hard cap)"},"409":{"description":"flow_not_confirmed"}}}},"/billing/checkout":{"post":{"summary":"Create a Stripe Checkout Session (7-day trial, card-first)","operationId":"create_billing_checkout","parameters":[{"name":"Idempotency-Key","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["workspace_id","tier"],"properties":{"workspace_id":{"type":"string","format":"uuid"},"tier":{"type":"string","enum":["starter","pro","team"]},"success_url":{"type":"string","format":"uri"},"cancel_url":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"checkout_url + session_id"},"401":{"description":"Auth required"},"403":{"description":"Bearer token does not match workspace_id"},"422":{"description":"Invalid tier"},"502":{"description":"Stripe checkout failed"}}}},"/billing/portal":{"post":{"summary":"Open the Stripe Customer Portal (cancel, update card)","operationId":"create_billing_portal","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["workspace_id"],"properties":{"workspace_id":{"type":"string","format":"uuid"},"return_url":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"portal_url"},"401":{"description":"Auth required"},"409":{"description":"No Stripe customer yet"}}}},"/billing/webhook":{"post":{"summary":"Stripe webhook receiver (signature-verified, idempotent)","operationId":"stripe_webhook","security":[],"responses":{"200":{"description":"Processed or duplicate"},"400":{"description":"Bad signature / missing event id"}}}},"/healthz":{"get":{"summary":"Health check","operationId":"healthz","security":[],"responses":{"200":{"description":"ok"}}}}}}