{"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.","x-service-info":{"title":"Prufa API","description":"Web QA audits, flows, monitors, and workspace management. Human and agent callers are first-class; the same product is reachable via the dashboard, HTTP API, CLI, and MCP server.","categories":["qa","monitoring","agent-platform","web-testing","subscription"],"payment_methods":["stripe"],"billing_model":"subscription_with_metered_overage","hard_cap_default":true,"trial":{"days":7,"card_collection":"always"}}},"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"}}}},"503":{"description":"at_capacity — free-audit pool saturated; nothing was queued, the call is replay-safe. Honor Retry-After.","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"}}}},"/reports/by-token/{share_token}":{"get":{"summary":"Get the JSON report payload for a run, by public share_token","operationId":"get_report_by_token","description":"Public-by-token lookup. The share_token is the unlisted public identifier in /r/<token> URLs. Lets the MCP server and any agent fetch a report without knowing the internal UUID. No auth required: possession of the token IS the authorization (same model as the public /r/<token> page).","parameters":[{"name":"share_token","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"JSON report"},"404":{"description":"No run with that share_token"}}}},"/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"}},"x-payment-info":{"intent":"subscription","method":"stripe","amount":"29.00","currency":"USD","description":"Persistent monitor deploys require a paid plan. Free-tier workspaces receive 402 with a checkout-bearing hint pointing at POST /api/v1/billing/checkout.","checkout_url":"https://prufa.dev/api/v1/billing/checkout","tiers":["starter","pro","team"]}},"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"}},"x-payment-info":{"intent":"subscription","method":"stripe","amount":"0.00","currency":"USD","description":"Workspace settings (alert channels, usage webhook, overage opt-in). opt_in_to_overage=true requires a paid tier; free-tier workspaces receive 402.","checkout_url":"https://prufa.dev/api/v1/billing/checkout"}}},"/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"}},"x-payment-info":{"intent":"metered","method":"stripe","amount":"0.10","currency":"USD","description":"Drafting a flow compiles a natural-language test case into a reviewable spec; the compilation itself can invoke the model. Replays of confirmed flows are free.","checkout_url":"https://prufa.dev/api/v1/billing/checkout"}},"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"}},"x-payment-info":{"intent":"metered","method":"stripe","amount":"0.10","currency":"USD","description":"One LLM call inside the agent loop = one metered run ($0.10/run on Starter after the included quota). Replays of confirmed flows whose pinned selectors still work are free (zero LLM calls, zero metered runs).","checkout_url":"https://prufa.dev/api/v1/billing/checkout","meter_event":"prufa_run_starter"}}},"/gremlin":{"post":{"summary":"Start a Gremlin chaos run — undirected, code-verified breakage discovery (Pro full run / free-tier teaser). Gated on GREMLIN_ENABLED.","operationId":"create_gremlin_run","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"},"persona":{"type":["string","null"],"enum":["confused_newbie","impatient","hostile","fat_finger","back_button_masher",null]},"direction":{"type":["string","null"],"maxLength":500,"description":"Optional freeform steer (untrusted)"}}}}}},"responses":{"202":{"description":"{run_id, status, persona, mode, step_cap, report_url, usage}"},"402":{"description":"quota_exceeded (metered as N units)"},"404":{"description":"gremlin_disabled (feature flag off)"},"422":{"description":"unsafe_url | invalid_persona"},"429":{"description":"too_many_gremlin_runs (concurrency cap)"}},"x-payment-info":{"intent":"metered","method":"stripe","amount":"0.10","currency":"USD","description":"A Gremlin chaos run is metered as several units. Step budget per run is tier-differentiated: free 8 (teaser), Starter and trial 20, Pro 40, Team 60. Each chaotic step is an LLM call counted against the included-runs quota.","checkout_url":"https://prufa.dev/api/v1/billing/checkout","meter_event":"prufa_run_pro"}}},"/gremlin/domains":{"get":{"summary":"List this workspace's per-domain mutation authorizations","operationId":"list_authorized_domains","responses":{"200":{"description":"{domains: [{host, allow_mutation, note}]}"}}},"put":{"summary":"Authorize (or revoke) real mutation for a host. Default is dry-run; payments are never executed regardless.","operationId":"authorize_domain","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["host"],"properties":{"host":{"type":"string"},"allow_mutation":{"type":"boolean","default":true},"note":{"type":["string","null"],"maxLength":256}}}}}},"responses":{"200":{"description":"{host, allow_mutation, note, warning}"},"422":{"description":"invalid_host"}}}},"/app-model/{domain}":{"get":{"summary":"Latest (or ?version=N) app model declared for a domain","operationId":"get_app_model","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"}},{"name":"version","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"{domain, version, source, doc, schema}"},"404":{"description":"app_model_not_found"}}},"put":{"summary":"Declare / update the app model for a registered domain. Append-only: every write bumps version; runs pin the version they used. Doc validates against docs/specs/app_model.schema.json.","operationId":"put_app_model","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"}},{"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":{"$ref":"https://prufa.dev/docs/specs/app_model.schema.json"}}}},"responses":{"200":{"description":"{domain, version, source, doc, schema}"},"404":{"description":"domain_not_registered"},"422":{"description":"invalid_app_model | invalid_domain"}}},"post":{"summary":"Alias of PUT (create-or-update; agents retry safely)","operationId":"post_app_model","parameters":[{"name":"domain","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Replays within 24h return the original response without side effects."}],"responses":{"200":{"description":"{domain, version, source, doc, schema}"},"404":{"description":"domain_not_registered"},"422":{"description":"invalid_app_model | invalid_domain"}}}},"/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"}},"x-payment-info":{"intent":"session","method":"stripe","amount":"29.00","currency":"USD","description":"Create a Stripe Checkout session for a paid plan. 7-day card-first trial; subscription auto-bills on day 8.","checkout_url":"https://prufa.dev/api/v1/billing/checkout","tiers":["starter","pro","team"]}}},"/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"}},"x-payment-info":{"intent":"self_service","method":"stripe","amount":"0.00","currency":"USD","description":"Open the Stripe-hosted Customer Portal to view invoices, update card, cancel subscription.","checkout_url":"https://prufa.dev/api/v1/billing/portal"}}},"/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"}}}}}}