# Shotwisp API — full documentation for machines Shotwisp is a developer HTTPS API for uploading and sharing images, capturing website screenshots, and rendering web pages as PDFs. This file is self-sufficient: everything needed to build a working integration is below. The same content is served as HTML at https://shotwisp.com/docs and as OpenAPI 3.1 at https://shotwisp.com/openapi.json. Base URL: https://shotwisp.com API version: v1 (stable; see "Versioning" below) # Authentication Every request needs an API key in the Authorization header: ``` Authorization: Bearer sw_4f8a09c2e7b1d6a35f90c48e21b7d3aa64c1e8f2 ``` - Keys start with `sw_` and are shown once, at creation. Create and revoke them at https://shotwisp.com/dashboard/api-keys (account signup: https://shotwisp.com/signup; email verification is required before keys work). - Keys are account-scoped: the per-minute rate limit and monthly quota are shared across every key on the account. - Requests without a valid key return 401 `{"error":{"code":"unauthorized"}}`. # Error contract Every error, on every endpoint, uses one envelope with a stable machine-readable code: ```json {"error": {"code": "validation_error", "message": "width: Too big: expected number to be <=3840"}} ``` | Code | HTTP status | Retryable | Meaning | |------|-------------|-----------|---------| | unauthorized | 401 | no | The API key is missing, malformed, revoked, or the account is suspended or unverified. Do not retry with the same key. Fix the Authorization header or create a new API key. | | quota_exceeded | 402 | no | The monthly upload or render quota is used up and overages are off or unavailable on the plan. Do not retry until the monthly quota resets, the plan is upgraded, or overages are enabled. | | method_not_allowed | 405 | no | The endpoint only accepts POST requests. Do not retry. Use POST. | | not_found | 404 | no | The requested path does not exist under /api/. Likely a typo in the endpoint URL. Do not retry. Check the endpoint path against /openapi.json. | | file_too_large | 413 | no | An uploaded file exceeds the 10 MB per-file limit. Do not retry unchanged. Reduce the file below the limit. | | batch_too_large | 413 | no | Combined file data in a batch exceeds 50 MB. Do not retry unchanged. Split the batch or reduce combined size. | | unsupported_type | 415 | no | The file's bytes do not match a supported image format (PNG, JPEG, GIF, WebP, SVG, AVIF). Do not retry unchanged. Convert the file to a supported image format. | | validation_error | 422 | no | A request field failed validation. The message states which field and why. Do not retry unchanged. The message names the failing field; fix it first. | | too_many_files | 422 | no | A batch contains more than 10 file parts. Do not retry unchanged. Send fewer files per batch. | | rate_limited | 429 | yes | The plan's per-minute request budget is spent. Rejected requests are not metered. Retry after waiting the number of seconds in the Retry-After header, then back off exponentially with jitter. | | concurrency_limited | 429 | yes | The plan's concurrent-capture cap is fully in use by renders still in progress. Retry after the Retry-After delay once an in-flight capture finishes. Reduce parallelism to the plan's concurrency cap. | | internal | 500 | yes | Unexpected error on Shotwisp's side. The request was not metered. Safe to retry with exponential backoff. | | capture_failed | 502 | yes | The target page could not be loaded or captured (unreachable, timed out, or blocked the renderer). Failed captures are not metered. Retry once or twice with backoff. If it persists, the target page cannot be captured — do not keep retrying. | | render_failed | 502 | yes | The target page could not be loaded or rendered as a PDF. Failed renders are not metered. Retry once or twice with backoff. If it persists, the target page cannot be rendered — do not keep retrying. | # Rate limits and quotas - Rate limit: fixed one-minute window per account, shared across all v1 endpoints and keys. Headers on every authenticated response: - `x-shotwisp-ratelimit-limit`: Requests allowed per minute on the account's plan. - `x-shotwisp-ratelimit-remaining`: Requests left in the current one-minute window. - `x-shotwisp-ratelimit-reset`: Seconds until the window rolls over and the budget refills. - On 429, the response carries `Retry-After` (seconds). Honor it, then back off exponentially with jitter. Rejected requests are never metered. - Monthly quota: uploads and renders (screenshots + PDFs share one render pool) are metered per calendar month, resetting the 1st at 00:00 UTC. Headers on metered responses: - `x-shotwisp-quota-limit`: Monthly allowance for this unit type on the account's plan. - `x-shotwisp-quota-remaining`: Units left before the monthly quota is spent (floors at 0). - `x-shotwisp-quota-usage-percent`: Usage this month as a whole-number percentage of quota. - `x-shotwisp-quota-warning`: 80, 90, or 100. Present only once usage has crossed that threshold. - `x-shotwisp-overage`: "true" when the unit just consumed was past quota and billed as an overage. For batch uploads, describes the most recent stored file. - Past quota: with overages enabled (paid plans, off by default) requests keep succeeding and bill per unit; otherwise the API returns 402 `quota_exceeded` until the month rolls over or the plan changes. # Plan limits | Plan | Price | Requests/min | Concurrent captures | Uploads/month | Renders/month | Max file | Link expiry | |------|-------|--------------|---------------------|---------------|---------------|----------|-------------| | Free | $0 | 5 | 1 | 50 | 250 | 10 MB | 30 days max | | Starter | $9.99/mo | 40 | 3 | 500 | 2000 | 10 MB | No expiry while subscribed | | Pro | $19.99/mo (launch price; standard $29.99/mo) | 80 | 6 | 5000 | 10000 | 10 MB | No expiry while subscribed | | Business | $49.99/mo (launch price; standard $79.99/mo) | 150 | 12 | 25000 | 50000 | 10 MB | No expiry while subscribed | Overage rates (per unit past quota, when enabled): Free: screenshot not available, upload not available; Starter: screenshot $0.008, upload $0.004; Pro: screenshot $0.005, upload $0.0025; Business: screenshot $0.003, upload $0.0015. Current pricing: https://shotwisp.com/pricing # Fixed limits - Max upload file size: 10 MB (every plan) - Supported upload formats: PNG, JPEG, GIF, WebP, SVG, AVIF (detected from file bytes) - Batch upload: 10 files and 50 MB combined per request - Upload link lifetime (expires_in): 60 s to 31,536,000 s (one year); omit on a paid plan for no expiry - Screenshot width: 320–3840 px (default 1440) - Screenshot height: 320–2160 px (default 900) - Screenshot formats: png (default), jpeg, webp - PDF paper formats: letter (default), a4, legal - PDF scale: 0.1–2 (default 1) - PDF delay_ms: 0–10,000 ms (default 0) - Target URL: Public http(s) URLs only; localhost, private networks, and cloud metadata hosts are rejected - Page navigation timeout: 25 s, plus up to 3 s network settle - Total render budget: 60 s per screenshot, 75 s per PDF - API request cap: 90 s end to end - Rate limit window: Fixed one-minute window per account, shared across all v1 endpoints and API keys - Quota reset: First day of each calendar month, 00:00 UTC - Stored captures: Screenshots and PDFs stay available at their hosted URL until you delete them from the dashboard - Anonymous quick-share uploads: Always expire within 24 hours (web upload page, not the API) # Capability matrix - Full-page screenshots: SUPPORTED. Set full_page: true. - Custom viewport size: SUPPORTED. width 320–3840, height 320–2160 (both endpoints; viewport object on PDF). - PNG / JPEG / WebP output: SUPPORTED. Screenshot format parameter. - JavaScript execution: SUPPORTED. Always on; pages render in a real headless browser. - Redirect following: SUPPORTED. Up to 5 redirect hops; every hop is re-validated against the private-network rules. - Wait strategy / extra delay: SUPPORTED. PDF only: wait_until and delay_ms. Screenshots always wait for load plus a short network settle. - Hosted result URLs: SUPPORTED. Set "response": "json" to receive a hosted URL instead of bytes. - PDF paper size, orientation, margins, scale: SUPPORTED. paper_format, landscape, margin, scale, print_background, prefer_css_page_size. - Mobile device emulation / device scale factor: NOT SUPPORTED. Not supported. Use a narrow viewport width for responsive layouts. - Custom HTTP headers or cookies on the target request: NOT SUPPORTED. Not supported; pages requiring authentication cannot be captured. - CSS selector / element capture: NOT SUPPORTED. Not supported. Capture the viewport or full page. - Dark mode forcing: NOT SUPPORTED. Not supported. Pages render with their default color scheme. - Ad or cookie-banner blocking: NOT SUPPORTED. Not supported. - Transparent background: NOT SUPPORTED. Not supported. Captures include the page background. - JPEG/WebP quality tuning: NOT SUPPORTED. Not supported. Encoder defaults are used. - Disabling JavaScript: NOT SUPPORTED. Not supported. - Idempotency keys: NOT SUPPORTED. Not supported. A client-side timeout may still have completed server-side; check quota headers on the next call or the dashboard before re-issuing expensive work. # Security behavior - Screenshot/PDF target URLs must be public http(s) addresses. Localhost, private and link-local IP ranges, cloud metadata endpoints, and non-http(s) schemes are rejected with `validation_error`; DNS is re-resolved and re-checked immediately before capture. - During rendering, every request the page makes — redirect hops (max 5), iframes, subresources — is re-validated against the same private-network rules. - Uploads are validated by byte signature, not filename or declared content type. SVG files are sanitized (scripts, event handlers, external references stripped) before storage. - Uploaded files are served from an isolated origin with X-Content-Type-Options: nosniff; SVGs additionally get a sandboxing Content-Security-Policy. - The `delete_url` returned by uploads deletes without further authentication — treat it as a secret. # Versioning - The current API version is v1; every path starts with /api/v1/. - v1 request/response contracts are stable: fields, defaults, enums, and error codes will not change meaning. New optional parameters and new response fields may be added — clients must tolerate unknown fields. - Breaking changes would ship under a new version path with the old version kept working through a published migration window. Deprecations are announced in the changelog: https://shotwisp.com/changelog # Guidance for autonomous callers - Do not retry validation (422), authentication (401), or payload (413/415) errors without changing the request. - Respect Retry-After when receiving HTTP 429; add exponential backoff with jitter for repeated 429s. - Use exponential backoff for transient 5xx failures (500, 502). If 502 capture_failed/render_failed persists after two retries, the target page cannot be captured — stop. - In rare infrastructure failures a 5xx can arrive without the JSON envelope (e.g. from the CDN during an outage); treat any non-envelope 5xx as transient and retry with backoff. - Batch uploads: when every file fails, the response status is 402/413/415/422/500 but the body is still the per-item result object — request-level failures use the plain error envelope. - Keep concurrent captures at or below the plan's concurrency cap to avoid 429 concurrency_limited. - Idempotency keys are not supported: a client-side timeout may still have completed (and been metered) server-side. Verify via quota headers or the dashboard before re-issuing expensive work. - Full agent guide: https://shotwisp.com/docs/ai-agents ## POST /api/v1/upload — Upload an image Stores an image sent as multipart/form-data and returns a share-page URL, a direct file URL, and a tokenized delete URL. The stored content type is detected from the file's bytes; SVG files are sanitized before storage. Each stored file consumes one upload unit of the monthly quota. Request content type: multipart/form-data Success status: 201 (The upload was stored.) Success content types: application/json Possible error codes: unauthorized, quota_exceeded, file_too_large, unsupported_type, validation_error, rate_limited, internal ### Parameters - `file` (file (binary); required) — The image, sent as a multipart form part named "file". Allowed formats: PNG, JPEG, GIF, WebP, SVG, AVIF — detected from the file's bytes, not its filename or declared content type. Maximum size 10 MB. - `expires_in` (integer; optional; range 60..31536000) — Lifetime of the link in seconds, from 60 (one minute) to 31,536,000 (one year). Defaults when omitted: no scheduled expiry on an active paid plan; the 30-day plan maximum on Free. Free-plan links are capped at 30 days regardless of the value sent. ### Example (curl) ``` curl -X POST https://shotwisp.com/api/v1/upload \ -H "Authorization: Bearer $SHOTWISP_API_KEY" \ -F "file=@./screenshot.png" \ -F "expires_in=86400" ``` ### Example (Python) ```python import os, requests res = requests.post( "https://shotwisp.com/api/v1/upload", headers={"Authorization": f"Bearer {os.environ['SHOTWISP_API_KEY']}"}, files={"file": open("screenshot.png", "rb")}, data={"expires_in": "86400"}, # optional ) res.raise_for_status() print(res.json()["url"]) ``` ### Example response — 201 Created ```json { "id": "u7Kd2mXq9RfW4bZn", "slug": "k7mwq2ax", "url": "https://shotwisp.com/i/k7mwq2ax", "file_url": "https://shotwisp.com/f/k7mwq2ax", "delete_url": "https://shotwisp.com/api/uploads/u7Kd2mXq9RfW4bZn/delete?token=Fj3kW9sLq2Xv7Rp4Tz8mNc5d", "expires_at": "2026-08-13T14:05:00.000Z", "size_bytes": 481290, "content_type": "image/png", "filename": "screenshot.png" } ``` ## POST /api/v1/upload/batch — Upload images in batch Stores up to 10 images (max 50 MB combined) from repeated multipart "file" parts. Files are processed in order and results are per-file: the response is 201 when every file succeeds and 207 when results are mixed, with each item carrying either the stored upload or its own error code. When every file fails, the status reflects the per-file failures (402, 413, 415, 422, or 500) but the body is still the per-item result object, not the error envelope — request-level failures (auth, rate limit, malformed multipart, batch size or count) use the plain error envelope. Each stored file consumes one upload unit of the monthly quota. Request content type: multipart/form-data Success status: 201 or 207 (Per-file results in request order. 201 when all files stored; 207 when some failed.) Success content types: application/json Possible error codes: unauthorized, quota_exceeded, batch_too_large, file_too_large, unsupported_type, too_many_files, validation_error, rate_limited, internal ### Parameters - `file` (file (binary, repeated); required) — One part per image, all named "file". Up to 10 files and 50 MB of combined file data per request; each file also has the individual 10 MB limit and byte-signature validation of the single-upload endpoint. - `expires_in` (integer; optional; range 60..31536000) — Lifetime of the link in seconds, from 60 (one minute) to 31,536,000 (one year). Defaults when omitted: no scheduled expiry on an active paid plan; the 30-day plan maximum on Free. Free-plan links are capped at 30 days regardless of the value sent. Applies to every file in the batch. ### Example (curl) ``` curl -X POST https://shotwisp.com/api/v1/upload/batch \ -H "Authorization: Bearer $SHOTWISP_API_KEY" \ -F "file=@./first.png" \ -F "file=@./second.jpg" \ -F "expires_in=86400" ``` ### Example (Python) ```python import os, requests res = requests.post( "https://shotwisp.com/api/v1/upload/batch", headers={"Authorization": f"Bearer {os.environ['SHOTWISP_API_KEY']}"}, files=[ ("file", open("first.png", "rb")), ("file", open("second.jpg", "rb")), ], data={"expires_in": "86400"}, # optional ) for item in res.json()["items"]: # res.status_code: 201 all stored, 207 mixed print(item["upload"]["url"] if item["ok"] else item["error"]["code"]) ``` ### Example response — 207 Multi-Status ```json { "items": [ { "ok": true, "index": 0, "source_filename": "first.png", "upload": { "id": "u7Kd2mXq9RfW4bZn", "slug": "k7mwq2ax", "url": "https://shotwisp.com/i/k7mwq2ax", "file_url": "https://shotwisp.com/f/k7mwq2ax", "delete_url": "https://shotwisp.com/api/uploads/u7Kd2mXq9RfW4bZn/delete?token=Fj3kW9sLq2Xv7Rp4Tz8mNc5d", "expires_at": "2026-08-13T14:05:00.000Z", "size_bytes": 481290, "content_type": "image/png", "filename": "first.png" } }, { "ok": false, "index": 1, "source_filename": "second.jpg", "error": { "code": "unsupported_type", "message": "Unsupported image type. The file's contents do not match a supported format." } } ], "summary": { "total": 2, "succeeded": 1, "failed": 1 } } ``` ## POST /api/v1/screenshot — Take a screenshot Loads the URL in a managed browser and returns the rendered image — by default the encoded bytes (Content-Type image/png, image/jpeg, or image/webp per `format`), or capture metadata plus a hosted image URL when `"response": "json"`. The page is navigated with JavaScript enabled, waiting for the load event plus up to 3s of network settle (navigation budget 25s, total render budget 60s). Each successful capture consumes one render unit of the monthly quota; failed captures are not metered. Request content type: application/json Success status: 200 (The rendered image bytes, or capture metadata in JSON mode.) Success content types: image/png, image/jpeg, image/webp, application/json Possible error codes: unauthorized, quota_exceeded, validation_error, rate_limited, concurrency_limited, internal, capture_failed ### Parameters - `url` (string; required) — Absolute URL of the page to capture. Public http(s) addresses only — localhost, private hosts, and internal IP ranges are rejected. - `full_page` (boolean; optional; default false) — Capture the full scrollable height of the page instead of just the viewport. width and height still set the viewport the page lays out in. - `format` (string; optional; default "png"; one of: png, jpeg, webp) — Output image encoding. - `width` (integer; optional; default 1440; range 320..3840) — Viewport width in CSS pixels. - `height` (integer; optional; default 900; range 320..2160) — Viewport height in CSS pixels. - `response` (string; optional; default "image"; one of: image, json) — "image" streams the encoded image bytes back; "json" returns capture metadata and a hosted image URL instead. ### Example (curl) ``` curl -X POST https://shotwisp.com/api/v1/screenshot \ -H "Authorization: Bearer $SHOTWISP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com", "full_page": true, "format": "png"}' \ -o shot.png ``` ### Example (Python) ```python import os, requests res = requests.post( "https://shotwisp.com/api/v1/screenshot", headers={"Authorization": f"Bearer {os.environ['SHOTWISP_API_KEY']}"}, json={"url": "https://example.com", "width": 1280, "height": 800}, timeout=90, ) res.raise_for_status() open("shot.png", "wb").write(res.content) # default response is image bytes ``` ### Example response — 200 OK — "response": "json" ```json { "id": "s3Nv8qLp5TkX2wYc", "url": "https://shotwisp.com/s/s3Nv8qLp5TkX2wYc", "format": "png", "width": 1280, "height": 800, "full_page": false, "duration_ms": 2140, "size_bytes": 208431 } ``` ## POST /api/v1/pdf — Render a PDF Loads the URL in a managed browser and prints it to PDF — by default the application/pdf bytes, or render metadata plus a hosted PDF URL when `"response": "json"`. `viewport` controls the responsive layout the page renders at; `paper_format` controls the printed sheet (navigation budget 25s, total render budget 75s). A successful PDF consumes one render unit of the monthly quota — the same unit type as screenshots; failed renders are not metered. Request content type: application/json Success status: 200 (The PDF bytes, or render metadata in JSON mode.) Success content types: application/pdf, application/json Possible error codes: unauthorized, quota_exceeded, validation_error, rate_limited, concurrency_limited, internal, render_failed ### Parameters - `url` (string; required) — Absolute URL of the page to render. Public http(s) addresses only — localhost, private hosts, and internal IP ranges are rejected. - `paper_format` (string; optional; default "letter"; one of: letter, a4, legal) — Paper size of the generated PDF. - `landscape` (boolean; optional; default false) — Rotate the selected paper size to landscape orientation. - `print_background` (boolean; optional; default true) — Include CSS background colors and images in the PDF. - `prefer_css_page_size` (boolean; optional; default false) — Let the page's CSS @page size override paper_format when the page defines one. - `scale` (number; optional; default 1; range 0.1..2) — Render scale applied to the page content. - `margin` (object; optional; default {"top":0,"right":0,"bottom":0,"left":0}) — Page margins. Each side accepts a non-negative number (CSS pixels) or a CSS length string with px, in, cm, or mm units. - `margin.top` (number | string; optional; default 0) — Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm. - `margin.right` (number | string; optional; default 0) — Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm. - `margin.bottom` (number | string; optional; default 0) — Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm. - `margin.left` (number | string; optional; default 0) — Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm. - `viewport` (object; optional; default {"width":1440,"height":900}) — Browser viewport applied before printing. Controls the page's responsive layout, not the paper dimensions. - `viewport.width` (integer; optional; default 1440; range 320..3840) — Viewport width in CSS pixels. - `viewport.height` (integer; optional; default 900; range 320..2160) — Viewport height in CSS pixels. - `wait_until` (string; optional; default "networkidle2"; one of: load, domcontentloaded, networkidle0, networkidle2) — Page readiness event to wait for before rendering. - `delay_ms` (integer; optional; default 0; range 0..10000) — Extra wait after page readiness, in milliseconds. Useful for animations or late client-side rendering. - `response` (string; optional; default "pdf"; one of: pdf, json) — "pdf" streams application/pdf bytes back; "json" returns render metadata and a hosted PDF URL instead. ### Example (curl) ``` curl -X POST https://shotwisp.com/api/v1/pdf \ -H "Authorization: Bearer $SHOTWISP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com", "paper_format": "a4"}' \ -o page.pdf ``` ### Example (Python) ```python import os, requests res = requests.post( "https://shotwisp.com/api/v1/pdf", headers={"Authorization": f"Bearer {os.environ['SHOTWISP_API_KEY']}"}, json={"url": "https://example.com", "paper_format": "a4", "landscape": True}, timeout=90, ) res.raise_for_status() open("page.pdf", "wb").write(res.content) # default response is PDF bytes ``` ### Example response — 200 OK — "response": "json" ```json { "id": "p8Fm3qRt7VwK2xNc", "url": "https://shotwisp.com/p/p8Fm3qRt7VwK2xNc", "paper_format": "a4", "landscape": false, "print_background": true, "prefer_css_page_size": false, "scale": 1, "margin": { "top": "0.5in", "right": "0.5in", "bottom": "0.5in", "left": "0.5in" }, "viewport": { "width": 1440, "height": 900 }, "wait_until": "networkidle2", "delay_ms": 0, "duration_ms": 1840, "size_bytes": 42718 } ``` # Support - Contact: https://shotwisp.com/contact - Dashboard: https://shotwisp.com/dashboard - Machine-readable spec: https://shotwisp.com/openapi.json