{"openapi":"3.1.0","info":{"title":"SnapDiff API","version":"1.0.0","description":"Visual diff & page monitoring API. Compare two web pages visually in one API call.","contact":{"url":"https://snapdiff.ai"}},"servers":[{"url":"https://api.snapdiff.ai/v1","description":"Production"},{"url":"http://localhost:3002/v1","description":"Local development (docker compose)"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"API key from the dashboard (sd_live_... or sd_test_...). Also accepted as `Authorization: Bearer <key>`."}},"schemas":{},"parameters":{}},"paths":{"/diff/baseline":{"post":{"tags":["Diff"],"summary":"Compare a URL against a stored baseline","description":"Resolves the baseline for {project, page_name, branch} (falling back to the project default branch when an exact match is missing) and runs a visual diff against the supplied `after` URL. Use this when you have a SnapDiff project set up and want to verify a page still matches its approved state — equivalent to /diff but with baseline lookup done server-side in one round trip.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"after":{"type":"string","minLength":1,"description":"URL, image URL, or screenshot ID (ss_xxx) for the \"after\" state to compare against the stored baseline","example":"https://staging.example.com/pricing"},"project":{"type":"string","minLength":1,"description":"Project slug or ID (prj_xxx)","example":"acme-marketing"},"page_name":{"type":"string","minLength":1,"description":"Page name within the project","example":"pricing"},"branch":{"type":"string","description":"Branch to look up the baseline on. Falls back to the project default branch and then to the project default branch baseline.","example":"main"},"threshold":{"type":"number","minimum":0,"maximum":1,"description":"Pixel sensitivity 0.0-1.0","example":0.1},"diff_color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","description":"Hex color for diff highlights","example":"#ff0000"},"ignore_regions":{"type":"array","items":{"type":"object","properties":{"x":{"type":"integer","minimum":0},"y":{"type":"integer","minimum":0},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}},"required":["x","y","width","height"]},"description":"Rectangular regions to exclude from comparison"},"ignore_selectors":{"type":"array","items":{"type":"string"},"description":"CSS selectors to mask before comparison","example":[".dynamic-date","#ad-banner"]},"antialiasing":{"type":"boolean","description":"Ignore sub-pixel rendering differences"},"screenshot_options":{"type":"object","properties":{"viewport":{"type":"object","properties":{"width":{"type":"integer","minimum":320,"maximum":3840},"height":{"type":"integer","minimum":240,"maximum":2160}}},"full_page":{"type":"boolean"},"block":{"type":"object","properties":{"ads":{"type":"boolean"},"cookie_banners":{"type":"boolean"}}}},"description":"Options applied when capturing the after screenshot"}},"required":["after","project","page_name"]}}}},"responses":{"200":{"description":"Diff result, including the baseline that was resolved","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"df_xyz789"},"match":{"type":"boolean"},"diff_percentage":{"type":"number","example":3.42},"diff_pixel_count":{"type":"integer"},"total_pixels":{"type":"integer"},"diff_image_url":{"type":"string","format":"uri"},"before_image_url":{"type":"string","format":"uri"},"after_image_url":{"type":"string","format":"uri"},"overlay_image_url":{"type":"string","format":"uri"},"changed_regions":{"type":"array","items":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"},"label":{"type":"string"},"centroid":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}],"description":"Pixel-mass centroid of the changed pixels inside this region."}},"required":["x","y","width","height","label"]}},"dimensions":{"type":"object","properties":{"width":{"type":"number"},"height":{"type":"number"}},"required":["width","height"]},"duration_ms":{"type":"integer"},"baseline":{"type":"object","properties":{"project_id":{"type":"string"},"project_slug":{"type":"string"},"page_name":{"type":"string"},"branch":{"type":"string"},"baseline_id":{"type":"string"}},"required":["project_id","project_slug","page_name","branch","baseline_id"]}},"required":["id","match","diff_percentage","diff_pixel_count","total_pixels","diff_image_url","before_image_url","after_image_url","overlay_image_url","changed_regions","dimensions","duration_ms","baseline"]}}}},"401":{"description":"Missing or invalid API key"},"402":{"description":"Diff quota exceeded"},"404":{"description":"Project or baseline not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"code":{"type":"string"}},"required":["message"]}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded"}}}},"/diff":{"post":{"tags":["Diff"],"summary":"Compare two URLs visually","description":"The core product endpoint. Send two URLs (or screenshot IDs), get back a diff percentage and highlighted diff image showing exactly what changed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"before":{"type":"string","minLength":1,"description":"URL, image URL, or screenshot ID (ss_xxx) for the \"before\" state","example":"https://example.com"},"after":{"type":"string","minLength":1,"description":"URL, image URL, or screenshot ID (ss_xxx) for the \"after\" state","example":"https://staging.example.com"},"threshold":{"type":"number","minimum":0,"maximum":1,"description":"Per-pixel color sensitivity 0.0-1.0 (pixelmatch threshold). Lower = stricter per-pixel comparison. This is NOT the match tolerance — see match_tolerance_percent.","example":0.1},"match_tolerance_percent":{"type":"number","minimum":0,"maximum":100,"description":"Maximum diff_percentage that still counts as match: true. Defaults to 0.1 (%). Set to 0 to flag any changed pixel as a mismatch.","example":0.1},"diff_color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","description":"Hex color for diff highlights","example":"#ff0000"},"ignore_regions":{"type":"array","items":{"type":"object","properties":{"x":{"type":"integer","minimum":0},"y":{"type":"integer","minimum":0},"width":{"type":"integer","minimum":1},"height":{"type":"integer","minimum":1}},"required":["x","y","width","height"]},"description":"Rectangular regions to exclude from comparison"},"ignore_selectors":{"type":"array","items":{"type":"string"},"description":"CSS selectors to mask before comparison","example":[".dynamic-date","#ad-banner"]},"antialiasing":{"type":"boolean","description":"Ignore sub-pixel rendering differences","example":true},"screenshot_options":{"type":"object","properties":{"viewport":{"type":"object","properties":{"width":{"type":"integer","minimum":320,"maximum":3840},"height":{"type":"integer","minimum":240,"maximum":2160}}},"full_page":{"type":"boolean"},"block":{"type":"object","properties":{"ads":{"type":"boolean"},"cookie_banners":{"type":"boolean"}}}},"description":"Options applied when capturing before/after screenshots"}},"required":["before","after"]}}}},"responses":{"200":{"description":"Diff result with images and changed regions","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"df_xyz789"},"match":{"type":"boolean","description":"true when diff_percentage is at or below match_tolerance_percent (default 0.1%)"},"diff_percentage":{"type":"number","example":3.42},"diff_pixel_count":{"type":"integer"},"total_pixels":{"type":"integer"},"diff_image_url":{"type":"string","format":"uri"},"before_image_url":{"type":"string","format":"uri"},"after_image_url":{"type":"string","format":"uri"},"overlay_image_url":{"type":"string","format":"uri"},"changed_regions":{"type":"array","items":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"},"label":{"type":"string"},"centroid":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}],"description":"Pixel-mass centroid of the changed pixels inside this region. Used by the verification verdict logic to handle layout-reflow cases where the bbox spans much more area than the actual change."}},"required":["x","y","width","height","label"]}},"dimensions":{"type":"object","properties":{"width":{"type":"number"},"height":{"type":"number"}},"required":["width","height"]},"duration_ms":{"type":"integer"}},"required":["id","match","diff_percentage","diff_pixel_count","total_pixels","diff_image_url","before_image_url","after_image_url","overlay_image_url","changed_regions","dimensions","duration_ms"]}}}},"401":{"description":"Missing or invalid API key"},"402":{"description":"Diff quota exceeded"},"429":{"description":"Rate limit exceeded"}}}},"/screenshot":{"post":{"tags":["Screenshot"],"summary":"Take a screenshot","description":"Capture a screenshot of a URL or render HTML to an image. Supporting infrastructure for the diff endpoint.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"URL to screenshot (required unless html is provided)","example":"https://example.com"},"html":{"type":"string","minLength":1,"description":"Raw HTML to render (alternative to url)"},"width":{"type":"integer","minimum":320,"maximum":3840,"description":"Viewport width","example":1280},"height":{"type":"integer","minimum":240,"maximum":2160,"description":"Viewport height","example":720},"format":{"type":"string","enum":["png","jpeg","webp"],"description":"Output image format","example":"webp"},"quality":{"type":"integer","minimum":1,"maximum":100,"description":"Image quality (ignored for png)","example":80},"scale":{"type":"integer","minimum":1,"maximum":3,"description":"Device scale factor (retina)","example":2},"full_page":{"type":"boolean","description":"Capture full scrollable page"},"dark_mode":{"type":"boolean","description":"Force dark color scheme"},"selector":{"type":"string","description":"CSS selector to capture only this element"},"block_ads":{"type":"boolean","description":"Block ad/tracker network requests. Unless block_cookie_banners is set explicitly, this also hides cookie banners and chat widgets.","example":true},"block_cookie_banners":{"type":"boolean","description":"Hide cookie consent banners and chat widgets via injected CSS, independently of block_ads. Defaults to following block_ads.","example":true},"delay_ms":{"type":"integer","minimum":0,"maximum":10000,"description":"Extra delay after page load in ms"},"cache_ttl":{"type":"integer","minimum":0,"description":"Cache duration in seconds (0 = skip cache)","example":86400}}}}}},"responses":{"200":{"description":"Screenshot captured successfully","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"ss_abc123"},"url":{"type":"string","format":"uri","description":"CDN URL of the screenshot"},"width":{"type":"integer"},"height":{"type":"integer"},"file_size":{"type":"integer","description":"File size in bytes"},"duration_ms":{"type":"integer"},"cached":{"type":"boolean"}},"required":["id","url","width","height","file_size","duration_ms","cached"]}}}},"401":{"description":"Missing or invalid API key"},"402":{"description":"Screenshot quota exceeded"},"429":{"description":"Rate limit exceeded"}}}},"/screenshot/upload":{"post":{"tags":["Screenshot"],"summary":"Upload a pre-captured screenshot","description":"Store a PNG captured client-side (e.g. by the Playwright reporter) and get back a screenshot ID usable in builds and diffs. Body is the raw PNG bytes with Content-Type: image/png — not JSON, not multipart. Max 25 MB. Counts toward the screenshot quota.","parameters":[{"schema":{"type":"string","description":"URL the screenshot was captured from (display/metadata only)"},"required":false,"name":"source_url","in":"query"},{"schema":{"type":"string","description":"Human-readable label (e.g. story ID or page name)"},"required":false,"name":"label","in":"query"}],"requestBody":{"required":true,"content":{"image/png":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"Screenshot stored","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Screenshot ID — pass as snapshots[].screenshot_id when creating a build","example":"ss_abc123"},"url":{"type":"string","format":"uri","description":"CDN URL of the stored screenshot"},"width":{"type":"integer"},"height":{"type":"integer"},"file_size":{"type":"integer","description":"File size in bytes"}},"required":["id","url","width","height","file_size"]}}}},"400":{"description":"Empty body or body is not a PNG"},"401":{"description":"Missing or invalid API key"},"402":{"description":"Screenshot quota exceeded"},"413":{"description":"Upload exceeds 25 MB"},"429":{"description":"Rate limit exceeded"}}}},"/projects":{"post":{"tags":["Projects"],"summary":"Create a project","description":"Create a new project to organize baselines and builds. Projects group visual regression tests for a single site or app.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable project name","example":"Marketing Site"},"slug":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$","description":"URL-safe identifier (lowercase, hyphens allowed)","example":"marketing-site"},"default_branch":{"type":"string","minLength":1,"maxLength":100,"description":"Default branch for baseline resolution (defaults to \"main\")","example":"main"}},"required":["name","slug"]}}}},"responses":{"201":{"description":"Project created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"prj_abc123"},"name":{"type":"string","example":"Marketing Site"},"slug":{"type":"string","example":"marketing-site"},"default_branch":{"type":"string","example":"main"},"created_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]}},"required":["id","name","slug","default_branch","created_at","updated_at"]}}}},"402":{"description":"Project limit reached on current plan"},"409":{"description":"Project with this slug already exists"}}},"get":{"tags":["Projects"],"summary":"List projects","description":"List all projects belonging to the authenticated user.","responses":{"200":{"description":"Array of projects","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"prj_abc123"},"name":{"type":"string","example":"Marketing Site"},"slug":{"type":"string","example":"marketing-site"},"default_branch":{"type":"string","example":"main"},"created_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]}},"required":["id","name","slug","default_branch","created_at","updated_at"]}}}}}}}},"/projects/{projectId}":{"get":{"tags":["Projects"],"summary":"Get project details","description":"Get a project with baseline and build counts.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"}],"responses":{"200":{"description":"Project details","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"prj_abc123"},"name":{"type":"string","example":"Marketing Site"},"slug":{"type":"string","example":"marketing-site"},"default_branch":{"type":"string","example":"main"},"created_at":{"type":["string","null"]},"updated_at":{"type":["string","null"]},"baseline_count":{"type":"integer","example":5},"build_count":{"type":"integer","example":12}},"required":["id","name","slug","default_branch","created_at","updated_at","baseline_count","build_count"]}}}},"404":{"description":"Project not found"}}},"patch":{"tags":["Projects"],"summary":"Update a project","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"example":"Updated Name"},"slug":{"type":"string","minLength":1,"maxLength":50,"pattern":"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$"},"default_branch":{"type":"string","minLength":1,"maxLength":100}}}}}},"responses":{"200":{"description":"Project updated","content":{"application/json":{"schema":{"type":"object","properties":{"updated":{"type":"boolean"},"id":{"type":"string"}},"required":["updated","id"]}}}},"404":{"description":"Project not found"},"409":{"description":"Slug conflict"}}},"delete":{"tags":["Projects"],"summary":"Delete a project","description":"Delete a project and all its baselines, builds, and snapshots (cascade).","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"}],"responses":{"200":{"description":"Project deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}},"required":["deleted","id"]}}}},"404":{"description":"Project not found"}}}},"/projects/{projectId}/github":{"put":{"tags":["GitHub Integration"],"summary":"Configure GitHub integration","description":"Set up GitHub commit status reporting for a project. Provide a PAT with `repo:status` scope and the repo name. SnapDiff will post commit statuses when builds complete.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"repo":{"type":"string","pattern":"^[a-zA-Z0-9_.-]+\\/[a-zA-Z0-9_.-]+$","description":"GitHub repo in \"owner/name\" format","example":"acme-corp/marketing-site"},"token":{"type":"string","minLength":1,"description":"GitHub personal access token with repo:status scope","example":"ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},"status_enabled":{"type":"boolean","description":"Whether to post commit statuses (default true)","example":true}},"required":["repo","token"]}}}},"responses":{"200":{"description":"GitHub config saved","content":{"application/json":{"schema":{"type":"object","properties":{"project_id":{"type":"string"},"repo":{"type":"string"},"status_enabled":{"type":"boolean"},"token_last4":{"type":"string","description":"Last 4 characters of the token (for display)","example":"•••• xxxx"},"created_at":{"type":["string","null"]}},"required":["project_id","repo","status_enabled","token_last4","created_at"]}}}},"404":{"description":"Project not found"}}},"get":{"tags":["GitHub Integration"],"summary":"Get GitHub integration config","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"}],"responses":{"200":{"description":"GitHub config","content":{"application/json":{"schema":{"type":"object","properties":{"project_id":{"type":"string"},"repo":{"type":"string"},"status_enabled":{"type":"boolean"},"token_last4":{"type":"string","description":"Last 4 characters of the token (for display)","example":"•••• xxxx"},"created_at":{"type":["string","null"]}},"required":["project_id","repo","status_enabled","token_last4","created_at"]}}}},"404":{"description":"Project not found or GitHub not configured"}}},"delete":{"tags":["GitHub Integration"],"summary":"Remove GitHub integration","description":"Remove GitHub config from a project. Commit statuses will no longer be posted.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"}],"responses":{"200":{"description":"Config removed","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"}},"required":["deleted"]}}}},"404":{"description":"Project not found"}}}},"/projects/{projectId}/builds":{"post":{"tags":["Builds"],"summary":"Create a build","description":"Start a visual regression test. Screenshots each page, diffs against baselines, and returns results. Processing is async — poll the build detail endpoint for results.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"branch":{"type":"string","minLength":1,"description":"Branch name (defaults to project default_branch)","example":"feature/new-header"},"commit_sha":{"type":"string","description":"Git commit SHA","example":"abc123def456"},"commit_message":{"type":"string","description":"Git commit message","example":"Update hero section"},"pull_request_url":{"type":"string","format":"uri","description":"GitHub/GitLab PR URL","example":"https://github.com/org/repo/pull/42"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Extra HTTP headers applied to every request during page capture. Used to authenticate against protected previews (Vercel deployment protection, Cloudflare Access, basic auth, etc). Header values are not persisted to the database.","example":{"x-vercel-protection-bypass":"secret-value","x-vercel-set-bypass-cookie":"true"}},"pages":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Page identifier (used for baseline matching)","example":"homepage"},"url":{"type":"string","format":"uri","description":"URL to screenshot (worker captures it). Mutually exclusive with screenshot_id.","example":"https://staging.myapp.com/"},"screenshot_id":{"type":"string","pattern":"^ss_","description":"Pre-captured screenshot id from POST /v1/screenshot/upload. Use when the customer is providing PNGs (e.g. via the Playwright reporter). Mutually exclusive with url.","example":"ss_abc123"}},"required":["name"]},"minItems":1,"maxItems":200,"description":"Pages to compare against baselines. Each page either references a URL (worker will capture) or a pre-uploaded screenshot id."}},"required":["pages"]}}}},"responses":{"202":{"description":"Build accepted for processing","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"project_id":{"type":"string"},"status":{"type":"string","example":"processing"},"total_snapshots":{"type":"integer"},"branch":{"type":"string"},"poll_url":{"type":"string","example":"/v1/projects/prj_abc/builds/bld_xyz"},"build_url":{"type":"string","example":"https://snapdiff.ai/dashboard/builds/bld_xyz"}},"required":["id","project_id","status","total_snapshots","branch","poll_url","build_url"]}}}},"402":{"description":"Build quota exceeded"},"404":{"description":"Project not found"}}},"get":{"tags":["Builds"],"summary":"List builds","description":"List builds for a project, optionally filtered by branch or status.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"},{"schema":{"type":"string","description":"Filter by branch name"},"required":false,"name":"branch","in":"query"},{"schema":{"type":"string","enum":["pending","processing","approved","changes_requested","failed"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","description":"Max results (default 20)"},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Pagination offset"},"required":false,"name":"offset","in":"query"}],"responses":{"200":{"description":"Array of builds","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"bld_xyz789"},"project_id":{"type":"string"},"branch":{"type":["string","null"],"example":"feature/new-header"},"commit_sha":{"type":["string","null"],"example":"abc123def"},"commit_message":{"type":["string","null"]},"pull_request_url":{"type":["string","null"]},"status":{"type":"string","enum":["pending","processing","approved","changes_requested","failed"],"example":"changes_requested"},"total_snapshots":{"type":"integer","example":5},"changed_count":{"type":"integer","example":2},"approved_count":{"type":"integer","example":3},"created_at":{"type":["string","null"]}},"required":["id","project_id","branch","commit_sha","commit_message","pull_request_url","status","total_snapshots","changed_count","approved_count","created_at"]}}}}},"404":{"description":"Project not found"}}}},"/projects/{projectId}/builds/inherit":{"post":{"tags":["Builds"],"summary":"Inherit a pre-approved build from an ancestor commit","description":"Copies the snapshots from a previously-approved build (identified by its commit SHA) into a new build on the target branch. The new build is marked `approved` and does not require manual review. Use this on push-to-default-branch after a PR merge to avoid a second approval click.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from_commit_sha":{"type":"string","minLength":1,"description":"Git SHA of the commit whose approved SnapDiff build to copy. Typically the merge commit's source-branch parent (i.e. `git rev-parse HEAD^2`).","example":"abc123def456"},"branch":{"type":"string","minLength":1,"description":"Branch the new build belongs to (e.g. \"main\").","example":"main"},"commit_sha":{"type":"string","minLength":1,"description":"Git SHA of the new commit (the merge commit).","example":"def456abc123"},"commit_message":{"type":"string","description":"Commit message of the new commit."},"pull_request_url":{"type":"string","format":"uri"}},"required":["from_commit_sha","branch","commit_sha"]}}}},"responses":{"201":{"description":"Inherited build created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"project_id":{"type":"string"},"inherited_from_build_id":{"type":"string"},"status":{"type":"string","enum":["approved"]},"total_snapshots":{"type":"integer"},"branch":{"type":"string"}},"required":["id","project_id","inherited_from_build_id","status","total_snapshots","branch"]}}}},"404":{"description":"No approved build found for `from_commit_sha`"}}}},"/projects/{projectId}/builds/{buildId}":{"get":{"tags":["Builds"],"summary":"Get build details","description":"Get a build with all its snapshots, diff images, and review status. Use this to poll for build completion and review results.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"},{"schema":{"type":"string","description":"Build ID","example":"bld_xyz789"},"required":true,"name":"buildId","in":"path"}],"responses":{"200":{"description":"Build with snapshots","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"bld_xyz789"},"project_id":{"type":"string"},"branch":{"type":["string","null"],"example":"feature/new-header"},"commit_sha":{"type":["string","null"],"example":"abc123def"},"commit_message":{"type":["string","null"]},"pull_request_url":{"type":["string","null"]},"status":{"type":"string","enum":["pending","processing","approved","changes_requested","failed"],"example":"changes_requested"},"total_snapshots":{"type":"integer","example":5},"changed_count":{"type":"integer","example":2},"approved_count":{"type":"integer","example":3},"created_at":{"type":["string","null"]},"snapshots":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"bsn_abc123"},"page_name":{"type":"string","example":"homepage"},"url":{"type":"string","example":"https://staging.myapp.com/"},"baseline_id":{"type":["string","null"]},"screenshot_id":{"type":["string","null"]},"screenshot_url":{"type":["string","null"],"example":"https://cdn.example.com/v1/ss_abc.png"},"diff_id":{"type":["string","null"]},"diff_image_url":{"type":["string","null"]},"overlay_image_url":{"type":["string","null"]},"diff_percentage":{"type":["number","null"],"example":2.1},"status":{"type":"string","enum":["pending","approved","rejected","unchanged","new"],"example":"pending"},"reviewed_by":{"type":["string","null"]},"reviewed_at":{"type":["string","null"]}},"required":["id","page_name","url","baseline_id","screenshot_id","screenshot_url","diff_id","diff_image_url","overlay_image_url","diff_percentage","status","reviewed_by","reviewed_at"]}}},"required":["id","project_id","branch","commit_sha","commit_message","pull_request_url","status","total_snapshots","changed_count","approved_count","created_at","snapshots"]}}}},"404":{"description":"Build or project not found"}}}},"/projects/{projectId}/builds/{buildId}/approve":{"post":{"tags":["Builds"],"summary":"Approve entire build","description":"Approve all pending snapshots in a build. Updates baselines with the new screenshots. This is the batch approval endpoint.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"},{"schema":{"type":"string","description":"Build ID","example":"bld_xyz789"},"required":true,"name":"buildId","in":"path"}],"responses":{"200":{"description":"Build approved","content":{"application/json":{"schema":{"type":"object","properties":{"approved":{"type":"boolean"},"build_id":{"type":"string"},"baselines_updated":{"type":"integer"}},"required":["approved","build_id","baselines_updated"]}}}},"404":{"description":"Build or project not found"}}}},"/projects/{projectId}/builds/{buildId}/snapshots/{snapshotId}/approve":{"post":{"tags":["Builds"],"summary":"Approve a single snapshot","description":"Approve one snapshot and update its baseline. Automatically recalculates the build status.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"},{"schema":{"type":"string","description":"Build ID","example":"bld_xyz789"},"required":true,"name":"buildId","in":"path"},{"schema":{"type":"string","description":"Snapshot ID","example":"bsn_abc123"},"required":true,"name":"snapshotId","in":"path"}],"responses":{"200":{"description":"Snapshot approved","content":{"application/json":{"schema":{"type":"object","properties":{"approved":{"type":"boolean"},"snapshot_id":{"type":"string"},"baseline_updated":{"type":"boolean"}},"required":["approved","snapshot_id","baseline_updated"]}}}},"404":{"description":"Snapshot, build, or project not found"}}}},"/projects/{projectId}/builds/{buildId}/snapshots/{snapshotId}/reject":{"post":{"tags":["Builds"],"summary":"Reject a single snapshot","description":"Reject a visual change. The baseline is NOT updated. Automatically recalculates the build status.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"},{"schema":{"type":"string","description":"Build ID","example":"bld_xyz789"},"required":true,"name":"buildId","in":"path"},{"schema":{"type":"string","description":"Snapshot ID","example":"bsn_abc123"},"required":true,"name":"snapshotId","in":"path"}],"responses":{"200":{"description":"Snapshot rejected","content":{"application/json":{"schema":{"type":"object","properties":{"rejected":{"type":"boolean"},"snapshot_id":{"type":"string"}},"required":["rejected","snapshot_id"]}}}},"404":{"description":"Snapshot, build, or project not found"}}}},"/projects/{projectId}/baselines":{"get":{"tags":["Baselines"],"summary":"List baselines","description":"List all current baselines for a project. Each baseline represents the last accepted screenshot for a page on a branch.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"},{"schema":{"type":"string","description":"Filter by branch name"},"required":false,"name":"branch","in":"query"}],"responses":{"200":{"description":"Array of baselines","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"bl_abc123"},"page_name":{"type":"string","example":"homepage"},"branch":{"type":"string","example":"main"},"screenshot_id":{"type":"string"},"screenshot_url":{"type":"string","example":"https://cdn.example.com/v1/ss_abc.png"},"accepted_by":{"type":["string","null"]},"accepted_at":{"type":["string","null"]},"created_at":{"type":["string","null"]}},"required":["id","page_name","branch","screenshot_id","screenshot_url","accepted_by","accepted_at","created_at"]}}}}},"404":{"description":"Project not found"}}}},"/projects/{projectId}/baselines/{baselineId}":{"delete":{"tags":["Baselines"],"summary":"Delete a baseline","description":"Delete a baseline. The next build will treat this page as new (no comparison). The underlying screenshot is not deleted.","parameters":[{"schema":{"type":"string","description":"Project ID","example":"prj_abc123"},"required":true,"name":"projectId","in":"path"},{"schema":{"type":"string","description":"Baseline ID","example":"bl_abc123"},"required":true,"name":"baselineId","in":"path"}],"responses":{"200":{"description":"Baseline deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}},"required":["deleted","id"]}}}},"404":{"description":"Baseline or project not found"}}}},"/verifications":{"post":{"tags":["Verifications"],"summary":"Create an agent verification","description":"Capture the `after` URL, diff it against the resolved baseline, compute a verdict from the agent's `intent`, and persist the result. Returns a `review_url` the human can open to approve or reject the change.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"after":{"type":"string","minLength":1,"description":"URL or screenshot ID (ss_xxx) for the page after the agent's change.","example":"http://localhost:3002/account"},"project":{"type":"string","minLength":1,"description":"Project slug or ID (prj_xxx).","example":"acme-app"},"page_name":{"type":"string","minLength":1,"description":"Page name within the project.","example":"account"},"branch":{"type":"string","description":"Branch to look up the baseline on. Defaults to the project's default branch.","example":"main"},"intent":{"type":"string","minLength":1,"description":"One sentence describing what the agent intended to change.","example":"added a Cancel subscription button to the bottom of the billing card"},"intent_regions":{"type":"array","items":{"type":"object","properties":{"bbox":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"},{"type":"number"},{"type":"number"}]},"selector":{"type":"string"},"label":{"type":"string"}}},"description":"Optional hints about where the change should appear. `bbox` enables geometric verdict matching."},"threshold":{"type":"number","minimum":0,"maximum":1,"description":"Per-pixel color sensitivity 0.0-1.0 (pixelmatch threshold). Default 0.1.","example":0.1},"match_tolerance_percent":{"type":"number","minimum":0,"maximum":100,"description":"Maximum diff percentage that still counts as 'no change.' Defaults to 0.01% for verifications (stricter than the diff engine's general 0.1% noise floor) so subtle styling regressions don't slip through as no_change_detected. Raise it for noisier pages, lower it for ultra-strict gates.","example":0.01},"ignore_selectors":{"type":"array","items":{"type":"string"},"description":"CSS selectors to mask before comparison (e.g. timestamps, dynamic counters)."},"full_page":{"type":"boolean","description":"Compare the full scrollable page. Default false."}},"required":["after","project","page_name","intent"]}}}},"responses":{"200":{"description":"Verification created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"vrf_abc123"},"status":{"type":"string","enum":["pending","approved","rejected"]},"verdict":{"type":"string","enum":["pass","expected_change_detected","unexpected_regression","no_change_detected","needs_human_review"]},"next_action":{"type":"string","enum":["proceed","request_human_review","rollback_and_retry","verify_deployment"]},"reasoning":{"type":"string"},"intent":{"type":"string"},"intent_regions":{"type":["array","null"],"items":{"type":"object","properties":{"bbox":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"},{"type":"number"},{"type":"number"}]},"selector":{"type":"string"},"label":{"type":"string"}}}},"diff_id":{"type":"string"},"diff_percentage":{"type":"number"},"diff_image_url":{"type":"string"},"before_image_url":{"type":"string"},"after_image_url":{"type":"string"},"annotated_regions":{"type":"array","items":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"},"label":{"type":"string"},"matches_intent":{"type":["boolean","null"]},"centroid":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}],"description":"Pixel-mass centroid of the changed pixels inside this region. Used by the verdict logic for reflow-robust matching."}},"required":["x","y","width","height","label","matches_intent"]}},"baseline":{"type":"object","properties":{"project_id":{"type":"string"},"project_slug":{"type":"string"},"page_name":{"type":"string"},"branch":{"type":"string"},"baseline_id":{"type":"string"}},"required":["project_id","project_slug","page_name","branch","baseline_id"]},"review_url":{"type":"string"},"reviewed_by":{"type":["string","null"]},"reviewed_at":{"type":["string","null"]},"reviewer_note":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","status","verdict","next_action","reasoning","intent","intent_regions","diff_id","diff_percentage","diff_image_url","before_image_url","after_image_url","annotated_regions","baseline","review_url","reviewed_by","reviewed_at","reviewer_note","created_at"]}}}},"401":{"description":"Missing or invalid API key"},"402":{"description":"Diff quota exceeded"},"404":{"description":"Project or baseline not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"code":{"type":"string"}},"required":["message"]}},"required":["error"]}}}},"429":{"description":"Rate limit exceeded"}}}},"/verifications/{id}":{"get":{"tags":["Verifications"],"summary":"Get a verification","parameters":[{"schema":{"type":"string","description":"Verification ID","example":"vrf_abc123"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Verification","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"vrf_abc123"},"status":{"type":"string","enum":["pending","approved","rejected"]},"verdict":{"type":"string","enum":["pass","expected_change_detected","unexpected_regression","no_change_detected","needs_human_review"]},"next_action":{"type":"string","enum":["proceed","request_human_review","rollback_and_retry","verify_deployment"]},"reasoning":{"type":"string"},"intent":{"type":"string"},"intent_regions":{"type":["array","null"],"items":{"type":"object","properties":{"bbox":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"},{"type":"number"},{"type":"number"}]},"selector":{"type":"string"},"label":{"type":"string"}}}},"diff_id":{"type":"string"},"diff_percentage":{"type":"number"},"diff_image_url":{"type":"string"},"before_image_url":{"type":"string"},"after_image_url":{"type":"string"},"annotated_regions":{"type":"array","items":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"},"label":{"type":"string"},"matches_intent":{"type":["boolean","null"]},"centroid":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}],"description":"Pixel-mass centroid of the changed pixels inside this region. Used by the verdict logic for reflow-robust matching."}},"required":["x","y","width","height","label","matches_intent"]}},"baseline":{"type":"object","properties":{"project_id":{"type":"string"},"project_slug":{"type":"string"},"page_name":{"type":"string"},"branch":{"type":"string"},"baseline_id":{"type":"string"}},"required":["project_id","project_slug","page_name","branch","baseline_id"]},"review_url":{"type":"string"},"reviewed_by":{"type":["string","null"]},"reviewed_at":{"type":["string","null"]},"reviewer_note":{"type":["string","null"]},"created_at":{"type":"string"}},"required":["id","status","verdict","next_action","reasoning","intent","intent_regions","diff_id","diff_percentage","diff_image_url","before_image_url","after_image_url","annotated_regions","baseline","review_url","reviewed_by","reviewed_at","reviewer_note","created_at"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"code":{"type":"string"}},"required":["message"]}},"required":["error"]}}}}}}},"/verifications/{id}/approve":{"post":{"tags":["Verifications"],"summary":"Approve a verification","description":"Promote the verification's `after` screenshot as the new baseline for (project, page_name, branch). Also promotes to the project default branch (matching the build approval flow) so a later merge doesn't require re-approval.","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":"string","maxLength":1000}}}}}},"responses":{"200":{"description":"Approved","content":{"application/json":{"schema":{"type":"object","properties":{"approved":{"type":"boolean","enum":[true]},"verification_id":{"type":"string"},"promoted_screenshot_id":{"type":"string"}},"required":["approved","verification_id","promoted_screenshot_id"]}}}},"404":{"description":"Verification not found"},"409":{"description":"Verification already reviewed","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"code":{"type":"string"}},"required":["message"]}},"required":["error"]}}}}}}},"/verifications/{id}/reject":{"post":{"tags":["Verifications"],"summary":"Reject a verification","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":"string","maxLength":1000}}}}}},"responses":{"200":{"description":"Rejected","content":{"application/json":{"schema":{"type":"object","properties":{"rejected":{"type":"boolean","enum":[true]},"verification_id":{"type":"string"}},"required":["rejected","verification_id"]}}}},"404":{"description":"Verification not found"},"409":{"description":"Verification already reviewed","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"code":{"type":"string"}},"required":["message"]}},"required":["error"]}}}}}}}},"webhooks":{}}