# ViperCapture complete machine-readable reference This document is normative product guidance for generating ViperCapture API requests. The live OpenAPI schema at https://capture.viperisuseful.cc/openapi.json is authoritative for JSON types and validation. The interactive schema is https://capture.viperisuseful.cc/reference. ## Endpoint and authentication - Method and URL: `POST https://capture.viperisuseful.cc/v1/render` - Content type: `application/json` - Authentication: `Authorization: Bearer vc_live_...` - API keys are created in the authenticated Dashboard and shown once. - Optional correlation header: `X-Request-Id`. It must be 1-128 characters and match `[A-Za-z0-9][A-Za-z0-9._:-]{0,127}`. - The request body is strict. Unknown fields are rejected. Never invent fields or use `/screenshot`; `/screenshot` intentionally returns 404. - The successful body is the artifact itself, not a JSON envelope. Use `Content-Type` and `Content-Disposition`. ## Durable async image jobs Use async jobs when the caller should not hold an HTTP render connection open: - `POST /v1/jobs`: same strict render body, limited to one PNG/JPEG/WebP with `cache:false`. Returns HTTP 202 with `Location`, `Retry-After: 1`, and `Cache-Control: private, no-store`. - `POST /v1/jobs/bulk`: best-effort submission of 1-10 independently idempotent job items in a body no larger than 6 MiB. Each item is `{ "id"?: string, "request_id": string, "render": RenderRequest }`. The envelope and items are strict, request IDs must be unique within the envelope, and the response is HTTP 200 with `{count, accepted, failed, results}` in input order. Accepted results contain `job`; rejected results contain the item's HTTP `status` and typed `error`. - `GET /v1/jobs/{job_id}`: authenticated status polling. Polls do not consume render attempts. - `GET /v1/jobs/{job_id}/result`: authenticated binary download after `succeeded`. - `DELETE /v1/jobs/{job_id}`: cancels only `queued` work. A `running` job returns `job_already_running`. Creation consumes one API attempt but does not reserve a render in-flight slot; workers acquire per-account and global render capacity only when execution begins. For safe transport retries, send a stable `X-Request-Id` to the single endpoint or `request_id` on each bulk item: the same ID with the exact same normalized request returns the original job, even at the active-job or render-concurrency limit; the same ID with a different body returns `idempotency_key_conflict`. The comparison uses a keyed HMAC fingerprint and stores no plaintext source or target-header values. Each repeated single POST or bulk item still consumes an API attempt. Bulk authentication happens once, then every item independently consumes an API attempt and applies the normal account/global active-job limits. A completed bulk submission returns HTTP 200, including mixed item outcomes. Acceptance is best effort rather than transactional: an item failure does not roll back earlier jobs or prevent later items from being checked. Retry only rejected items with their original request IDs. Active async-job limits equal plan API concurrency: Free 1, Starter 3, Builder 10. The global durable queue holds 30 queued/running jobs. Queued work must start within 15 minutes. Two background workers feed the same account-fair API browser scheduler, retain the website's reserved capacity, and acquire the normal per-account in-flight allowance before rendering. Result delivery is limited to two concurrent downloads service-wide and holds that allowance until streaming completes or disconnects. Status values: `queued`, `running`, `succeeded`, `failed`, `cancelled`, `expired`. Every status includes `attempts`. A successful status includes `result_url`, result media metadata, credit cost/source, queue/render milliseconds, and `result_expires_at`. Honor `Retry-After: 1` and stop polling at terminal status. Result retrieval before success returns 409 `job_not_ready`; an expired or missing result returns 410 `async_result_expired`. URL/HTML/Markdown input and target headers are protected with AES-GCM authenticated encryption before Neon storage and erased at terminal state or queue expiry. Retryable failures use bounded backoff for at most three execution attempts and never extend the original queue deadline. Database claims reuse a stable acknowledgement token after ambiguous failures. All reservation, staging, completion, failure, and requeue mutations compare the expected attempt number, so a delayed worker cannot overwrite a newer retry. Exhausted work terminates as `async_attempts_exhausted` with `retryable:false`. Credits reserve only when a worker starts, each failed attempt releases its reservation, and credits settle after successful result upload. Interrupted unstaged work releases its reservation and requeues when attempts remain; a staged delivery is finalized after restart without rendering or charging twice. Successful images live in UploadThing for four hours and are proxied through the authenticated result endpoint after ownership, expiry, media, and output-size validation. Provider fetching is cancelled on client disconnect. Job metadata is deleted after 24 hours. No webhook callback is implemented. ## Source selection Exactly one source is required: - `url`: public HTTP(S) URL. - `html`: inline HTML, maximum 5 MiB UTF-8. - `markdown`: inline CommonMark-compatible Markdown, maximum 5 MiB UTF-8. `base_url` is optional only with HTML or Markdown. It must be a public HTTP(S) URL and resolves relative assets and links. It is invalid with `url`. Hosted SSRF protection checks the initial target, redirects, and subresources. Private, loopback, link-local, carrier-grade NAT, benchmark, multicast, reserved, and DNS-rebinding targets are blocked. Hosted service workers and WebSockets are blocked. ## Complete request object All defaults below preserve the behavior of requests written before these features existed. - `url`: URL or null. Exactly one source. - `html`: string or null. Exactly one source; max 5 MiB UTF-8. - `markdown`: string or null. Exactly one source; max 5 MiB UTF-8. - `base_url`: URL or null. Inline sources only. - `engine`: enum, default `chromium`. Values: `chromium`, `firefox`, `webkit`. PDF and `image.optimize_for_speed` require Chromium. - `output`: enum, default `png`. Values: `png`, `jpeg`, `webp`, `avif`, `gif`, `webm`, `mp4`, `pdf`, `html`, `markdown`, `metadata`. - `viewport`: object, default `{width:1280,height:720,device_scale_factor:1}`. - `width`, `height`: schema range 1-65535 CSS pixels; account execution limits are stricter. - `device_scale_factor`: schema range 0.1-8; account execution limits are stricter. - `viewports`: null or array of 2-3 named viewport objects. Produces a ZIP. See multi-viewport section. - `environment`: object, default `{device:"desktop",color_scheme:null,reduced_motion:null,media:null,locale:null,timezone:null}`. `media` is null, `screen`, or `print`; null keeps screen media for screenshots and print media for PDFs. - `network`: object for `java_script_enabled` (default `true`), `user_agent`, geolocation, up to 64 cookies, up to 64 URL block patterns, resource-type blocking, CSP bypass, and HTTPS-error handling. Set `java_script_enabled` to `false` to suppress target-page scripts; ViperCapture's browser controls remain available. Hosted SSRF protections remain active. `network.proxy` is rejected on hosted service. - `actions`: ordered array of at most 32 `click`, `hover`, `fill`, `press`, `select`, `scroll`, `wait`, `hide`, or `javascript` actions. Per-action delays are 0-15000 ms and timeouts 1-30000 ms. - `assertions`: object containing up to 32 unique `content_includes`, `content_excludes`, and `request_failures` strings per list. Failed assertions return typed non-retryable errors. - `delivery.webhook_url`: always null on hosted synchronous rendering; webhook delivery is rejected. - `diagnostics`: `{bundle:false,include_console:true,include_network:true,include_har:false,include_trace:false,include_warc:false}`. HAR, trace, and WARC require `bundle:true`; trace is unavailable for video. - `deterministic`: `{enabled:false,timestamp_ms:1700000000000,random_seed:1,wait_for_fonts:true}`. Enabled mode fixes time/randomness and reduces motion. - `certification`: `{enabled:false}`. Enabled mode returns an Ed25519-signed artifact ZIP. It cannot be combined with viewport packs. Public key: `GET /v1/certification/public-key`. - `slices`: null or `{height,overlap}` for a single full-page image. Height 100-10000 CSS pixels; overlap 0-1000 and smaller than height. - `profile_id`, `save_profile`: persistent profiles are rejected on hosted service. - `video`: null or settings for GIF/WebM/MP4: `duration_ms` 1000-30000, optional stepped scrolling, and transparency for full-page GIF/WebM only. - `full_page`: boolean, default true. - `preserve_viewport_width`: boolean, default false. - `lazy_load`: enum, default `thorough`. Values: `thorough`, `adaptive`, `none`. - `selector`: string or null, 1-2048 characters. - `clip`: object or null. - `x`, `y`: numbers 0-100000 CSS pixels, default 0. - `width`, `height`: numbers greater than 0 and no greater than 20000 CSS pixels. - `custom_css`: string or null, maximum 65536 UTF-8 bytes. - `fail_on_status`: array of at most 32 unique integer HTTP codes from 100 through 599, default empty. - `image`: object, always accepted with defaults. - `quality`: null or integer 1-100. Only JPEG/WebP/AVIF. - `width`, `height`: null or integer 1-65535. Resize a single image; one omitted dimension preserves aspect ratio. - `transparent_background`: boolean, default false. Only PNG/WebP/AVIF. - `optimize_for_speed`: boolean, default false. Only Chromium PNG/WebP. - `pdf`: object or null. Required/default-created for PDF; invalid otherwise. Set `tagged:true` to ask Chromium to include structural tags for assistive technology and document parsing. Tagging depends on the source markup and is not PDF/UA certification. - `mode`: `print` or `single_page`, default `print`. - `paper_size`: `A0`, `A1`, `A2`, `A3`, `A4`, `A5`, `A6`, `Legal`, `Letter`, or `Tabloid`; default `A4`. - `orientation`: `portrait` or `landscape`, default `portrait`. - `print_background`: boolean, default true. - `margins`: top/right/bottom/left numbers 0-4 inches, each default 0.4. - `header_template`, `footer_template`: null or HTML strings up to 16384 characters each. - `page_ranges`: null or comma-separated pages/ranges selecting at most 50 pages; print mode only. - `extract_mode`: `document` or `article`, default `document`. Article is valid only for HTML/Markdown output. - `include_shadow_dom`: boolean, default false. Includes bounded open shadow roots in HTML/Markdown extraction only. - `headers`: object of string values, default empty. - Maximum 32 entries. - Each name 1-128 UTF-8 bytes and valid HTTP token syntax. - Each value maximum 4096 UTF-8 bytes and no control characters. - Total serialized maximum 16384 bytes. - ViperCapture rejects Host, hop-by-hop headers, Proxy-*, Sec-*, and X-Forwarded-*. - Custom Authorization/Cookie reaches only the exact initial origin and is stripped from cross-origin redirects/subresources. - `wait_for`: object. - `event`: `domcontentloaded`, `load`, or `networkidle`; default `load`. - `selector`: null or 1-2048 characters. - `selector_state`: `visible`, `attached`, `hidden`, or `detached`; default `visible`. - `text`: null or 1-4096 characters; wait until body innerText contains it. - `images`: boolean, default false; wait until every current image has completed loading or failed, bounded by `timeout_ms`. - `delay_ms`: integer 0-15000, default 0. - `timeout_ms`: integer 1-30000, default 15000; guest/free execution maximum is 15000. - `cleanup`: object. - `consent_mode`: `none`, `reject`, `accept`, or `hide`; default `none`. - `block_ads`, `block_trackers`, `block_chats`, `block_newsletters`: booleans, each default false. - `cache`: boolean, default false. Opt-in exact account-scoped 15-minute cache; valid only for a single PNG/JPEG/WebP output. - `proceed_on_captcha`: boolean, default false. True captures a visible challenge; never solves/bypasses it. ## Environment profiles `environment.device` values: - `desktop`: normal desktop signals for the selected engine. - `iphone_14`: installed Playwright iPhone 14 user-agent, touch, and mobile signals. - `pixel_7`: installed Playwright Pixel 7 user-agent, touch, and mobile signals. - `ipad`: installed Playwright iPad generation 7 user-agent, touch, and mobile signals. The service always overrides profile viewport and density with the explicit `viewport` object. Device selection does not silently change file dimensions. Common CSS viewports: iPhone 14 390x664, Pixel 7 412x839, iPad 810x1080. `color_scheme`: null/default, `light`, `dark`, `no-preference`. `reduced_motion`: null/default, `reduce`, `no-preference`. `media`: null/output default, `screen`, or `print`. Explicit media applies before navigation or inline content loads. Null keeps screen for screenshots and print for PDFs. `locale`: null or BCP-47-style 2-64 character language tag, e.g. `en-US`. `timezone`: null or valid IANA name, e.g. `UTC`, `Europe/Paris`, `America/New_York`. All profiles use the service's pinned selected engine, not physical Safari/Android hardware. Device profiles do not control IP location, bandwidth, OS fonts, cookies, or server experiments; use `network.geolocation` for browser geolocation. ## Custom CSS, selector, and rectangular clip Custom CSS is applied to the main document after navigation/consent setup and before waits and final capture. It does not pierce cross-origin iframes. External resources referenced by CSS still pass SSRF routing. Invalid/unapplicable CSS returns `custom_css_invalid`. Maximum 64 KiB. Selector requires image output and `full_page:false`. It captures the first visible match. Clip requires image output and `full_page:false`. Coordinates use final-document CSS pixels. The entire rectangle must be in bounds or `clip_out_of_bounds` is returned with document dimensions. Clip and selector are mutually exclusive. ## Navigation status and diagnostics For URL input, `fail_on_status` compares the main navigation's exact status. A match returns HTTP 424, code `target_status_failed`, detail `target_status`, and `X-ViperCapture-Navigation-Status`. Status 429 and 5xx matches are retryable; other matches are not. Inline HTML/Markdown has no navigation status, so no status can match. Successful response diagnostic headers: - `X-ViperCapture-Queue-Ms`: integer account-fair queue time. - `X-ViperCapture-Render-Ms`: integer renderer time after admission. - `X-ViperCapture-Output-Count`: 1, 2, or 3. - `X-ViperCapture-Navigation-Status`: main status when URL input is available. - `X-ViperCapture-Width`, `X-ViperCapture-Height`: final image pixel dimensions for single images. - `X-ViperCapture-Blocked-Subresources`: renderer-observed blocked unsafe subresources. - `X-ViperCapture-Cache`: `HIT`, `MISS`, or `MISS-NOT-STORED` when cache was requested. - `X-ViperCapture-Cache-Expires-In`: remaining entry lifetime in seconds. - `X-ViperCapture-Cache-Credit-Pending`: whether one free cached hit is awaiting a partner. - `X-ViperCapture-Cache-Credit-Pending-Expires-In`: remaining pairing window when pending. - `X-Request-Id`: request correlation identifier. Final URL is not put in HTTP headers to avoid leaking query secrets into logs. Metadata JSON includes final URL. ## Metadata output Request `output:"metadata"`. Response is `application/json`, filename `vipercapture-metadata.json`, cost 1 credit. It describes the final DOM after waits, CSS, cleanup, CAPTCHA checks, and lazy loading: - `schema_version`: currently 1. - `source_type`: `url`, `html`, or `markdown`. - `final_url`: final page URL. - `navigation_status`: integer or null. - `blocked_subresources`: integer. - `title`, `description`, `canonical_url`, `language`, `robots`, `theme_color`: string or null. - `open_graph`: object of first-seen `og:*` meta property values, max 32. - `twitter`: object of first-seen `twitter:*` meta name values, max 32. - `icons`: max 16 objects with `rel`, resolved `href`, `sizes`, `type`. - `headings`: max 100 objects with numeric `level` 1-6 and `text`. - `links.total`: total number of anchors with href. - `links.sample`: first 100 objects with text and resolved href. Strings are trimmed and capped at 2048 characters except keys/selected small attributes. Metadata is one page only; it does not crawl links or assign an SEO score. ## Multi-viewport ZIP packs Set `viewports` to 2 or 3 items and `full_page:false`. Output must be PNG/JPEG/WebP. Selector and clip are forbidden. Each item contains: - `name`: unique 1-32 characters matching `[A-Za-z0-9_-]+`. - `width`, `height`, `device_scale_factor`: same bounds as viewport. - `device`: optional profile, default `desktop`. Items render sequentially with fresh isolated contexts inside one queue admission and one overall hard deadline. They do not occupy multiple browser slots. API cost equals output count (2 or 3). Any item failure aborts the entire pack and releases its reservation. Response: `application/zip`, filename `vipercapture-viewports.zip`. Entries are `.` plus `manifest.json`. Manifest schema version 1 includes output, count, and each file's name, media type, pixel dimensions, and navigation status. Manifest omits source URL and headers. Archive byte total must fit the plan. ## Output types and credit cost - PNG/JPEG/WebP/AVIF single image: 1 credit. - GIF/WebM/MP4 video: 1 credit. - HTML/Markdown/metadata: 1 credit. - Diagnostic, certified, or sliced ZIP: 1 credit. - PDF: 2 credits. - 2-image viewport ZIP: 2 credits. - 3-image viewport ZIP: 3 credits. Credits reserve before queue/render and settle only after successful artifact validation/delivery. Failure, cancellation, timeout, or CAPTCHA rejection releases the reservation. For a single PNG/JPEG/WebP, `cache:true` stores an exact account-scoped result in UploadThing for 15 minutes. A miss costs the normal 1 credit. Hits bypass the browser and pair account-wide at two hits per credit: the first hit costs 0 and opens a 15-minute pending half; the second hit in that window costs 1 and clears it. If the half expires, that lone hit remains free. Cache hits still count as API attempts. Pairing is atomic across concurrent requests, cache keys, and API keys belonging to the account. Cache metadata stores only an HMAC fingerprint, UploadThing file key, media metadata, size, and expiry—not request sources or headers. Monthly API credits: - Free: 1500; 5 attempts/minute; 50/hour; 1 in flight. - Starter: 10000; 15/minute; 300/hour; 3 in flight. - Builder: 30000; 30/minute; 900/hour; 10 in flight. Shared service capacity: max 3 browser renders across Chromium, Firefox, and WebKit. API has 2 reserved slots and may borrow third while website idle. API queue max 30, website queue max 10, queue wait max 60 seconds, at most 1 website render. A responsive pack occupies one slot sequentially. ## Artifact behavior - PNG: `image/png`, `.png` - JPEG: `image/jpeg`, `.jpg` - WebP: `image/webp`, `.webp` - AVIF: `image/avif`, `.avif` - GIF: `image/gif`, `.gif` - WebM: `video/webm`, `.webm` - MP4: `video/mp4`, `.mp4` - PDF: `application/pdf`, `.pdf` - HTML: `text/html; charset=utf-8`, `.html` - Markdown: `text/markdown; charset=utf-8`, `.md` - Metadata: `application/json`, `.json` - Viewport, diagnostic, certified, and sliced bundles: `application/zip`, `.zip` Full-page max height: 20000 CSS pixels. Account viewport/output dimensions and global pixel limit also apply. `preserve_viewport_width:true` clips horizontal overflow to requested width while retaining full document height. Print PDF may select at most 50 pages. Single-page PDF maximum one page and 20000 CSS pixels high. PDF requires Chromium. Signed-in website oversized PNG/JPEG/WebP may use a temporary unguessable UploadThing URL for four hours. Opt-in API image caching uses separate 15-minute UploadThing objects but continues to return the binary artifact through `/v1/render`. Guest artifacts, ZIP, PDF, HTML, Markdown, and metadata do not use temporary delivery. ## Error envelope All API errors are JSON: `{"error":{"code":"stable_code","message":"human message","request_id":"id","retryable":false,"details":{}}}` Retry only when `retryable:true`, honoring `Retry-After`. Do not retry validation, auth, SSRF, output limit, clip conflict, or unsupported contract errors without changing the request. Common codes: - `invalid_request` / validation 422 - `unauthorized` 401 - `forbidden` 403 - `request_id_conflict` 409 for synchronous renders; `idempotency_key_conflict` 409 for an async request ID reused with another body - `captcha_detected` 409 - `target_status_failed` 424 - `credits_exhausted` 429 - `api_rate_limited` 429 - `capture_queue_busy` 503 - `async_output_unsupported`, `async_cache_unsupported` 422 - `async_account_limit` 429; `async_queue_full` 503 - `job_not_found` 404; `job_not_ready`, `job_already_running` 409 - `async_attempts_exhausted` terminal job error after three execution attempts - `async_result_expired` 410 - `target_timeout`, `render_timeout`, wait timeouts 504 - `target_not_public`, `redirect_not_public`, `subresource_not_public` - `selector_not_found` 404 - `clip_out_of_bounds` 422 - `custom_css_invalid` 422 - `output_dimensions_exceeded`, `pixel_limit_exceeded`, `page_too_tall`, `output_too_large` 413 ## Browser workspace presets UI presets are browser-local only, max 12, names max 40, storage key `vipercapture:capture-presets:v1`. They include safe output, viewport, environment, wait timing, cleanup, extraction, encoding, and pack settings. They include crop coordinates and failure-status policy. They deliberately exclude source URL/document, base URL, target headers, custom CSS, selector, wait selector, and wait text. Share links put only the same safe fields in a base64url JSON `preset` query parameter. They do not automatically run a capture. Base64 is not encryption. ## Site authorization and anti-bot systems ViperCapture does not bypass Cloudflare, CAPTCHAs, bot protection, authentication, WAFs, or rate limits. If you control the target, create the narrowest exception for ViperCapture requests to the required paths/methods. Prefer a dedicated hostname or authenticated same-origin header over broad IP allowlisting. Keep normal SSRF and authentication controls. Test on staging, log the rule, rate-limit it, and set an expiry/review owner. Detailed provider patterns: https://capture.viperisuseful.cc/docs/site-access ### Caller-managed external challenge tools For a target the caller administers or has explicit permission to test, the caller may independently use an authorized external challenge tool and then submit a new render with only short-lived, target-scoped state in `network.cookies` or `headers`. This is composition guidance, not an integration: ViperCapture does not call, select, recommend, or endorse an external tool; send challenge data to one; accept its credentials; or solve or bypass challenges. There is no automatic handoff or provider callback. Never send a ViperCapture API key or an external tool credential as target state. Hosted target scoping remains unchanged. Custom headers are sent only to the exact origin in `url`, so redirects and subresources on a different origin do not receive them. Cookie domains must match the requested hostname or one of its parent domains. Normal SSRF and outbound network checks remain active. Prefer short-lived, revocable state and never reuse a person's long-lived session. Full guidance: https://capture.viperisuseful.cc/docs/site-access#external-challenge-tools ## Safe minimal examples Single screenshot: `{"url":"https://example.com","output":"png"}` Dark mobile screenshot: `{"url":"https://example.com","output":"png","viewport":{"width":390,"height":664,"device_scale_factor":1},"environment":{"device":"iphone_14","color_scheme":"dark"},"full_page":false}` Status-enforced metadata: `{"url":"https://example.com","output":"metadata","fail_on_status":[404,429,500,502,503],"wait_for":{"event":"load","timeout_ms":15000}}` Rectangular invoice crop: `{"url":"https://example.com/invoice","output":"png","full_page":false,"clip":{"x":100,"y":200,"width":800,"height":1000}}` Responsive pack: `{"url":"https://example.com","output":"webp","full_page":false,"viewports":[{"name":"desktop","width":1280,"height":720},{"name":"tablet","width":768,"height":1024,"device":"ipad"},{"name":"mobile","width":390,"height":720,"device":"iphone_14"}]}`