# ViewportWitness by Apex Labs — Agent Usage Guide ViewportWitness is a browser QA API for AI agents. Submit a public HTTPS URL and receive screenshots, accessibility findings, layout analysis, and a structured report across three browser viewports (phone portrait, phone landscape, desktop). ## Choose a tool by intent - check_page: broad website testing, screenshots, responsive layout, accessibility, console, network, and performance evidence. - verify_page: explicit pass/fail website assertions. Use this instead of check_page only when the caller supplies assertions. - compare_page: visual regression and QA comparison against a completed baseline check. - extract_page: deterministic public HTML-to-Markdown extraction without browser execution. - web_release_gate: passive review of security headers, cookie flags, mixed content, cross-origin script integrity, and server disclosure. This is not a penetration test. - get_report: free polling and retrieval for a previously submitted job. The live service runs at https://qa.honeygate.app. Standard checks cost $0.08 USDC, read-only assertion checks cost $0.10, and baseline comparisons cost $0.12. Base and a separately enabled Solana rail may also appear in the live payment requirements. Local self-hosted instances can run in test mode without any payment. ## Base URL Live service: https://qa.honeygate.app Local dev default: http://localhost:3000 (test mode, no payment required) ## Quick start 1. POST /v1/checks — submit a URL with an x402 payment header, get a job ID 2. GET /v1/checks/:id — poll until status is "complete" 3. GET /v1/checks/:id/screenshots/:viewport — download a PNG screenshot Agent-native alternative: connect a Streamable HTTP MCP client to POST /mcp and use check_page, verify_page, compare_page, or the free get_report tool. Paid MCP tools use the standard x402 MCP payment metadata exchange. ### Unpaid probe (see 402 requirements) ``` curl -X POST https://qa.honeygate.app/v1/checks \ -H 'Content-Type: application/json' \ -d '{"url": "https://example.com"}' ``` Returns HTTP 402 with a PAYMENT-REQUIRED header containing x402 payment requirements. ### Payment-enabled client (x402 SDK) ```js import { wrapFetchWithPayment } from '@x402/fetch' const fetch402 = wrapFetchWithPayment(fetch, configuredX402Client) const res = await fetch402('https://qa.honeygate.app/v1/checks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://example.com' }), }) const job = await res.json() // { id, status: 'queued', pollUrl, paymentMode } ``` No private keys or wallet credentials belong in this service. The payment is made client-side. ## Create a check POST /v1/checks Content-Type: application/json PAYMENT-SIGNATURE: Idempotency-Key: my-unique-key-123 (optional, prevents duplicate jobs) { "url": "https://example.com" } Strict schema: only `url` is accepted. Extra fields are rejected (422). Response (202 Accepted): { "id": "550e8400-e29b-41d4-a716-446655440000", "status": "queued", "pollUrl": "/v1/checks/550e8400-e29b-41d4-a716-446655440000", "paymentMode": "testnet" | "production" } ## Additional products POST /v1/verify with `{ "url": "https://example.com", "assertions": [...] }` runs 1–20 read-only assertions across all viewports. Types: textVisible, selectorExists, selectorVisible, titleIncludes, noHorizontalOverflow, noConsoleErrors. POST /v1/compare with `{ "url": "https://example.com", "baselineJobId": "..." }` compares new screenshots and QA findings with a completed, unexpired baseline job. Diff PNGs are at GET /v1/checks/:id/diffs/:viewport. ## Poll for results GET /v1/checks/550e8400-e29b-41d4-a716-446655440000 While running: { "id": "...", "status": "queued" | "running", "createdAt": "2026-09-24T12:00:00.000Z", "pollUrl": "/v1/checks/..." } When complete, the full QAReport is returned: { "id": "...", "url": "https://example.com", "status": "PASS" | "FAIL" | "INCONCLUSIVE", "paymentMode": "testnet" | "production", "createdAt": "...", "expiresAt": "...", "checksPerformed": ["screenshot", "accessibility:axe-core", ...], "limitations": [], "contentHash": "sha256hex...", "summary": { "totalViolations": 2, "criticalViolations": 0, "totalErrors": 0, "overallLoadStatus": "success" }, "verdict": { "decision": "safe_to_ship" | "review" | "failed", "blockingIssues": 0, "warnings": 0, "reasons": [], "recommendedActions": [] }, "diagnosis": { "overview": "Plain-English summary derived from the collected evidence.", "findings": [{ "code": "responsive-layout", "severity": "high", "viewports": ["phonePortrait"], "diagnosis": "...", "fix": "...", "locatorHints": ["body:nth-of-type(1) > main:nth-of-type(1)"] }] }, "viewports": { "phonePortrait": { ... ViewportResult ... }, "phoneLandscape": { ... ViewportResult ... }, "desktop": { ... ViewportResult ... } } } ## ViewportResult fields - viewport: "phonePortrait" | "phoneLandscape" | "desktop" - dimensions: { width, height } (375x812, 812x375, 1440x900) - loadStatus: "success" | "timeout" | "error" - loadTimeMs: number - finalUrl: string (after redirects) - redirectCount: number - screenshotUrl: "/v1/checks/:id/screenshots/:viewport" - screenshotDimensions: { w, h } - screenshotBytes: number - screenshotSha256: string - consoleErrors: string[] (max 50, URLs redacted) - pageCrash: boolean - failedRequests: [{ url, status, reason }] (max 50, paths redacted) - overflowDetected: boolean - offscreenElements: number - layoutLocatorHints: string[] (max 5; structural tags and nth-of-type only) - performance.navigation: bounded DNS, connection, request, response, DOM, and load timings when available - performance.paint: first paint, first contentful paint, largest contentful paint, and cumulative layout shift when available - performance.resources: { requestCount, transferredBytes } observed during the existing page load - accessibility.violations: [{ id, impact, description, helpUrl, nodes, count }] - accessibility.completed: boolean - accessibility.passes: number - accessibility.incomplete: number - interactionObservations: { visibleControls, focusableControls, keyboardReachable } ## Get a screenshot GET /v1/checks/:id/screenshots/phonePortrait GET /v1/checks/:id/screenshots/phoneLandscape GET /v1/checks/:id/screenshots/desktop Returns PNG image (Content-Type: image/png). Available once the check is complete. ## Status values - queued — job accepted, waiting for worker - running — browser check in progress - complete — report available - failed — check failed permanently (see error field) - retryable — check timed out, may be re-submitted ## Report status values - PASS — complete evidence with no load, browser, network, layout, or serious accessibility failure - FAIL — a load, browser, network, layout, or critical/serious accessibility failure was found - INCONCLUSIVE — partial results or check errors ## Payment modes - testnet — Base Sepolia and, when advertised, Solana Devnet via Coinbase CDP. - production — Base mainnet and, when advertised, Solana mainnet; $0.08 USDC via x402. - test — No payment required. Only available in local/self-hosted instances. NOTE: test mode does NOT represent a real payment settlement. In paid modes, include a PAYMENT-SIGNATURE header with a valid x402 payment: PAYMENT-SIGNATURE: See GET https://qa.honeygate.app/.well-known/x402 for payment requirements. ## Idempotency Include an Idempotency-Key header to avoid creating duplicate jobs on retries: Idempotency-Key: my-client-generated-key-123 Same key returns the same job ID. Max length 128 characters. ## Error codes - invalid_url — URL rejected (not HTTPS, has credentials, etc.) - blocked_destination — URL resolves to a blocked IP range - validation_error — request body failed validation - worker_unavailable — worker is not accepting jobs - payment_required — x402 payment required (production/testnet mode) - not_found — job or screenshot not found - invalid_viewport — viewport name not recognized ## Full API spec GET /openapi.json — OpenAPI 3.1 machine-readable contract GET /skill.md — Concise agent skill manifest ## Customer feedback https://github.com/Baffles78/viewport-witness/issues/new?template=customer-feedback.yml Do not include private target URLs, wallet details, payment signatures, API keys, or other secrets in public feedback. Completed reports also return this URL as `feedbackUrl`.