Developers

API reference

Everything the Shotwisp API does, on one page: authenticated image uploads, website screenshots, PDFs, errors, rate limits, and quotas. Plain HTTPS in, JSON out.

Overview

Introduction

The Shotwisp API stores images and renders public web pages as screenshots or PDFs. Every request is plain HTTPS; every response is JSON unless you ask for raw bytes.

Base URL
https://shotwisp.com

All paths on this page are relative to the base URL. Uploads support single and batch routes; screenshots and PDFs each have a render route. Every endpoint requires an API key, which you create in the dashboard under API keys.

Each endpoint also has a dedicated page with Node.js and Python examples: /upload, /upload/batch, /screenshot, and /pdf.

Machine-readable versions of everything on this page: the OpenAPI 3.1 specification at /openapi.json, a concise llms.txt index, and the full text docs at /llms-full.txt. Building with an AI agent? Start at the AI agent guide.

Start here

Quick start

From zero to a captured screenshot in three steps.

  1. Create an account (free, no card) and generate a key under API keys.
  2. Send it on every request as Authorization: Bearer YOUR_API_KEY.
  3. Make your first screenshot request:
First request
curl -X POST https://shotwisp.com/api/v1/screenshot \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "response": "json"}'

The response is capture metadata plus a hosted image URL:

200 OK
{
  "id": "s3Nv8qLp5TkX2wYc",
  "url": "https://shotwisp.com/s/s3Nv8qLp5TkX2wYc",
  "format": "png",
  "width": 1440,
  "height": 900,
  "full_page": false,
  "duration_ms": 2140,
  "size_bytes": 208431
}

Drop "response": "json" to receive the PNG bytes directly instead.

Security

Authentication

Every API request is authenticated with a bearer key. Keys are scoped to your account and can be revoked at any time.

Pass the key in the Authorization header of every request. Keys start with sw_ and are shown once, at creation — keep the value in an environment variable rather than in source control.

Header format
Authorization: Bearer sw_4f8a09c2e7b1d6a35f90c48e21b7d3aa64c1e8f2
cURL
# store the key once, e.g. in your shell profile or .env
export SHOTWISP_API_KEY="sw_4f8a09c2e7b1d6a35f90c48e21b7d3aa64c1e8f2"

# every request carries the same header
curl https://shotwisp.com/api/v1/screenshot \
  -H "Authorization: Bearer $SHOTWISP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "response": "json"}'
Requests without a valid key return 401 unauthorized. Revoking a key in the dashboard takes effect immediately.

Endpoint · 01

Upload an image

Send an image as multipart form data, get back a share link that renders a clean viewer page, plus direct file and delete URLs.

POST/api/v1/upload

Request

Send multipart/form-data with a file part. Uploads count against the monthly quota of your plan.

ParameterTypeDefaultDescription
filerequiredfile (binary)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_inintegerLifetime 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. Range: 60–31,536,000.
cURL
curl -X POST https://shotwisp.com/api/v1/upload \
  -H "Authorization: Bearer $SHOTWISP_API_KEY" \
  -F "file=@./screenshot.png" \
  -F "expires_in=86400"
JavaScript
const form = new FormData();
form.append("file", file); // File or Blob
form.append("expires_in", "86400"); // optional

const res = await fetch("https://shotwisp.com/api/v1/upload", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SHOTWISP_API_KEY}` },
  body: form,
});

const upload = await res.json();
console.log(upload.url); // https://shotwisp.com/i/k7mwq2ax

Response

Success returns 201 Created. url is the share page, file_url serves the raw image, and delete_url removes the upload without further authentication. content_type is the type we detected in the bytes, which is not necessarily the one you sent.

201 Created
{
  "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-11T14:05:00.000Z",
  "size_bytes": 481290,
  "content_type": "image/png",
  "filename": "screenshot.png"
}
Treat delete_url as a secret — anyone holding it can delete the upload. expires_at is null when an active paid plan has no scheduled link expiry. If paid access ends, those links receive a 30-day grace period.

Batch uploads

POST/api/v1/upload/batch

Repeat the file part to upload up to 10 images in one request, with at most 50 MB of combined file data. expires_in applies to every file in the batch, while the normal per-file plan limit and monthly upload quota still apply individually.

ParameterTypeDefaultDescription
filerequiredfile (binary, repeated)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_inintegerLifetime 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. Range: 60–31,536,000.
cURL — two files
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"

Results stay in request order. The endpoint returns 201 Created when every file succeeds, or 207 Multi-Status when some files succeed and others fail. Request-level authentication, parsing, size, and rate-limit failures retain their normal 4xx response.

207 Multi-Status
{
  "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-11T14: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 }
}

Validation and serving

Files are validated by signature, not by filename or declared content type: a .png that does not carry PNG bytes is rejected with 415 unsupported_type. SVG uploads are sanitized before storage — scripts, event handlers, embedded objects, external entities, and off-document references are stripped, and the sanitized bytes are what we keep.

file_url hands off to the CDN origin, so image bytes never come from the app origin. Every file is served with X-Content-Type-Options: nosniff, and SVG additionally with Content-Security-Policy: sandbox.

Endpoint · 02

Take a screenshot

Describe a capture in JSON and receive the rendered image back — or a JSON summary of the capture, if you only need metadata.

POST/api/v1/screenshot

Request

Send a JSON body. Only url is required; everything else has a sensible default.

ParameterTypeDefaultDescription
urlrequiredstringAbsolute URL of the page to capture. Public http(s) addresses only — localhost, private hosts, and internal IP ranges are rejected.
full_pagebooleanfalseCapture the full scrollable height of the page instead of just the viewport. width and height still set the viewport the page lays out in.
formatstring"png"Output image encoding. One of: png, jpeg, webp.
widthinteger1440Viewport width in CSS pixels. Range: 320–3,840.
heightinteger900Viewport height in CSS pixels. Range: 320–2,160.
responsestring"image""image" streams the encoded image bytes back; "json" returns capture metadata and a hosted image URL instead. One of: image, json.
cURL — save the image
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

Response

By default the response streams the encoded image with the matching Content-Type. If the target page cannot be loaded, the request fails with 502 capture_failed — see Errors. Failed captures are not metered.

Image responses carry the capture id in x-shotwisp-id and the render time in x-shotwisp-duration-ms, alongside the usual quota and rate-limit headers.

Set response to json to receive capture metadata instead of bytes. The url field is the hosted image — a link to the stored capture, not the page you captured.

cURL — JSON mode
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", "width": 1280, "height": 800, "response": "json"}'
200 OK
{
  "id": "s3Nv8qLp5TkX2wYc",
  "url": "https://shotwisp.com/s/s3Nv8qLp5TkX2wYc",
  "format": "png",
  "width": 1280,
  "height": 800,
  "full_page": false,
  "duration_ms": 2140,
  "size_bytes": 208431
}

Endpoint · 03

Render a PDF

Turn a public web page into a print-ready PDF with focused controls for paper, layout, responsive viewport, and page readiness.

POST/api/v1/pdf

Request

Send a JSON body. Only url is required. A successful PDF consumes one screenshot unit because it uses the same managed-browser render capacity.

ParameterTypeDefaultDescription
urlrequiredstringAbsolute URL of the page to render. Public http(s) addresses only — localhost, private hosts, and internal IP ranges are rejected.
paper_formatstring"letter"Paper size of the generated PDF. One of: letter, a4, legal.
landscapebooleanfalseRotate the selected paper size to landscape orientation.
print_backgroundbooleantrueInclude CSS background colors and images in the PDF.
prefer_css_page_sizebooleanfalseLet the page's CSS @page size override paper_format when the page defines one.
scalenumber1Render scale applied to the page content. Range: 0.1–2.
marginobject{"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.topnumber | string0Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm.
margin.rightnumber | string0Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm.
margin.bottomnumber | string0Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm.
margin.leftnumber | string0Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm.
viewportobject{"width":1440,"height":900}Browser viewport applied before printing. Controls the page's responsive layout, not the paper dimensions.
viewport.widthinteger1440Viewport width in CSS pixels. Range: 320–3,840.
viewport.heightinteger900Viewport height in CSS pixels. Range: 320–2,160.
wait_untilstring"networkidle2"Page readiness event to wait for before rendering. One of: load, domcontentloaded, networkidle0, networkidle2.
delay_msinteger0Extra wait after page readiness, in milliseconds. Useful for animations or late client-side rendering. Range: 0–10,000.
responsestring"pdf""pdf" streams application/pdf bytes back; "json" returns render metadata and a hosted PDF URL instead. One of: pdf, json.
cURL — save the PDF
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",
    "print_background": true,
    "margin": { "top": "0.5in", "right": "0.5in", "bottom": "0.5in", "left": "0.5in" }
  }' \
  -o page.pdf

Response

By default, the response streams application/pdf bytes. It carries x-shotwisp-id, render duration, quota, and rate-limit headers. Failed renders are not metered.

Set response to json for metadata and a hosted url that opens the stored PDF.

cURL — JSON mode
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", "landscape": true, "response": "json"}'
200 OK
{
  "id": "p8Fm3qRt7VwK2xNc",
  "url": "https://shotwisp.com/p/p8Fm3qRt7VwK2xNc",
  "paper_format": "letter",
  "landscape": true,
  "print_background": true,
  "prefer_css_page_size": false,
  "scale": 1,
  "margin": { "top": 0, "right": 0, "bottom": 0, "left": 0 },
  "viewport": { "width": 1440, "height": 900 },
  "wait_until": "networkidle2",
  "delay_ms": 0,
  "duration_ms": 1840,
  "size_bytes": 42718
}
viewport chooses the page’s responsive breakpoint. paper_format chooses the printed sheet. They are intentionally separate.

Reference

Errors

Errors are boring on purpose. Every failure returns the same JSON envelope with a stable, machine-readable code — and every code has fixed retry semantics.

CodeHTTP statusRetryableMeaning
unauthorized401noThe 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_exceeded402noThe 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_allowed405noThe endpoint only accepts POST requests. Do not retry. Use POST.
not_found404noThe 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_large413noAn uploaded file exceeds the 10 MB per-file limit. Do not retry unchanged. Reduce the file below the limit.
batch_too_large413noCombined file data in a batch exceeds 50 MB. Do not retry unchanged. Split the batch or reduce combined size.
unsupported_type415noThe 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_error422noA 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_files422noA batch contains more than 10 file parts. Do not retry unchanged. Send fewer files per batch.
rate_limited429yesThe 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_limited429yesThe 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.
internal500yesUnexpected error on Shotwisp's side. The request was not metered. Safe to retry with exponential backoff.
capture_failed502yesThe 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_failed502yesThe 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.
Error response
{
  "error": {
    "code": "quota_exceeded",
    "message": "Monthly screenshot quota exceeded (250/250). Upgrade your plan to continue."
  }
}
Rule of thumb: 4xx codes (except 429) never succeed on an unchanged retry — fix the request first. 429 is retryable after Retry-After; 500 and 502 are retryable with exponential backoff. The full mapping is also machine-readable in the OpenAPI spec and expanded in the AI agent guide.

Limits

Rate limits

Each plan carries a sustained per-minute request budget and a ceiling on captures running at once. The budget is one counter per account, shared by every key and both endpoints.

PlanRequests / minuteConcurrent captures
Free51
Starter403
Pro806
Business15012

Successful calls carry your position in the current window, as do the 402 and 429 responses.

HeaderMeaning
x-shotwisp-ratelimit-limitRequests allowed in the current minute on your plan.
x-shotwisp-ratelimit-remainingRequests left in the current window.
x-shotwisp-ratelimit-resetSeconds until the window rolls over and the budget refills.

Once the budget is spent the API answers 429 rate_limited with Retry-After in seconds. Rejected requests are not metered and do not count against your monthly quota.

429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Retry-After: 27
x-shotwisp-ratelimit-limit: 80
x-shotwisp-ratelimit-remaining: 0
x-shotwisp-ratelimit-reset: 27

{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry in 27s."}}

Wait at least Retry-After seconds, then back off exponentially with jitter. Retrying immediately on a fixed interval keeps you pinned at the limit.

JavaScript — retry with backoff
async function callWithBackoff(send, attempts = 5) {
  for (let attempt = 0; ; attempt++) {
    const res = await send();
    if (res.status !== 429 || attempt === attempts - 1) return res;

    // Honour Retry-After, then back off exponentially with jitter.
    const retryAfter = Number(res.headers.get("retry-after")) || 1;
    const waitMs =
      Math.max(retryAfter, 2 ** attempt) * 1000 + Math.random() * 250;
    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }
}

Limits

Quotas and overages

Usage is metered per calendar month against your plan. Past quota, requests either keep succeeding and bill per unit or stop with 402 — that choice is the overages setting on your account.

PlanUploads / monthScreenshots / monthOverage / screenshotOverage / upload
Free50250
Starter5002,000$0.008$0.004
Pro5,00010,000$0.005$0.0025
Business25,00050,000$0.003$0.0015

Quotas reset on the first day of each calendar month at 00:00 UTC. Every plan accepts files up to 10 MB. Full plan details are on the pricing page.

Overages

With overages enabled, requests past quota succeed as normal and each extra unit is billed at your plan’s per-unit rate; the response carries x-shotwisp-overage: true so you can count billable units yourself. With overages disabled — the default — the API returns 402 quota_exceeded until the month rolls over or you upgrade. Free has no overage rate, so it always stops at the quota.

Overages are toggled per account in Billing. The change applies to the next request — nothing is retroactive.

Usage headers

Every metered call — upload or screenshot, image bytes or JSON — reports your quota position as of that request, so you can react before the limit rather than after it.

HeaderMeaning
x-shotwisp-quota-limitMonthly allowance for this unit type on your plan.
x-shotwisp-quota-remainingUnits left before the quota is spent. Reaches 0 and stays there.
x-shotwisp-quota-usage-percentUsage so far this month, as a whole-number percentage of quota.
x-shotwisp-quota-warning80, 90, or 100. Present only once usage has crossed that threshold.
x-shotwisp-overagetrue when the unit you just used was past quota and billed as an overage.
Response headers — 88% of a Pro quota
HTTP/1.1 200 OK
Content-Type: image/png
x-shotwisp-id: s3Nv8qLp5TkX2wYc
x-shotwisp-ratelimit-limit: 80
x-shotwisp-ratelimit-remaining: 78
x-shotwisp-ratelimit-reset: 41
x-shotwisp-quota-limit: 10000
x-shotwisp-quota-remaining: 1180
x-shotwisp-quota-usage-percent: 88
x-shotwisp-quota-warning: 80

x-shotwisp-quota-warning appears at 80, 90, and 100 percent of quota; the same thresholds drive the dashboard warnings. A 402 quota_exceeded response still carries x-shotwisp-quota-limit and x-shotwisp-quota-remaining.

Reference

All limits

Every fixed limit in one place. Per-plan numbers (rate limits, quotas, concurrency) are in the two sections above and on the pricing page — the values here apply to every plan.

LimitValue
Max upload file size10 MB (every plan)
Supported upload formatsPNG, JPEG, GIF, WebP, SVG, AVIF (detected from file bytes)
Batch upload10 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 width320–3840 px (default 1440)
Screenshot height320–2160 px (default 900)
Screenshot formatspng (default), jpeg, webp
PDF paper formatsletter (default), a4, legal
PDF scale0.1–2 (default 1)
PDF delay_ms0–10,000 ms (default 0)
Target URLPublic http(s) URLs only; localhost, private networks, and cloud metadata hosts are rejected
Page navigation timeout25 s, plus up to 3 s network settle
Total render budget60 s per screenshot, 75 s per PDF
API request cap90 s end to end
Rate limit windowFixed one-minute window per account, shared across all v1 endpoints and API keys
Quota resetFirst day of each calendar month, 00:00 UTC
Stored capturesScreenshots and PDFs stay available at their hosted URL until you delete them from the dashboard
Anonymous quick-share uploadsAlways expire within 24 hours (web upload page, not the API)

Reference

Capture capabilities

What the screenshot and PDF renderers do and — just as important — what they deliberately don't. Only features listed as supported exist in production.

FeatureSupportedNotes
Full-page screenshotsyesSet full_page: true.
Custom viewport sizeyeswidth 320–3840, height 320–2160 (both endpoints; viewport object on PDF).
PNG / JPEG / WebP outputyesScreenshot format parameter.
JavaScript executionyesAlways on; pages render in a real headless browser.
Redirect followingyesUp to 5 redirect hops; every hop is re-validated against the private-network rules.
Wait strategy / extra delayyesPDF only: wait_until and delay_ms. Screenshots always wait for load plus a short network settle.
Hosted result URLsyesSet "response": "json" to receive a hosted URL instead of bytes.
PDF paper size, orientation, margins, scaleyespaper_format, landscape, margin, scale, print_background, prefer_css_page_size.
Mobile device emulation / device scale factornoNot supported. Use a narrow viewport width for responsive layouts.
Custom HTTP headers or cookies on the target requestnoNot supported; pages requiring authentication cannot be captured.
CSS selector / element capturenoNot supported. Capture the viewport or full page.
Dark mode forcingnoNot supported. Pages render with their default color scheme.
Ad or cookie-banner blockingnoNot supported.
Transparent backgroundnoNot supported. Captures include the page background.
JPEG/WebP quality tuningnoNot supported. Encoder defaults are used.
Disabling JavaScriptnoNot supported.
Idempotency keysnoNot 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.

Reference

Security behavior

Guardrails that apply to every capture and upload, designed for callers that submit arbitrary URLs — including autonomous agents.

  • Capture targets must be public http(s) URLs. Localhost, private and link-local IP ranges, cloud metadata endpoints, and other schemes are rejected with validation_error, and DNS is re-resolved and re-checked immediately before the browser dials out.
  • During rendering, every request the page makes — redirect hops (maximum 5), iframes, and subresources — is re-validated against the same private-network rules, so a page cannot pull internal hosts into a capture.
  • Your API key is never forwarded to the target page; the renderer sends no authentication of yours.
  • Uploads are validated by byte signature rather than filename or declared type, and SVG files are sanitized (scripts, event handlers, and external references stripped) before storage.
  • Stored files are served from an isolated origin with X-Content-Type-Options: nosniff; SVGs additionally carry a sandboxing Content-Security-Policy.
  • The delete_url on uploads works without further authentication — treat it as a secret.

Reference

Versioning policy

Integrations — human-written or agent-generated — stay deployed for years. The API is versioned so they keep working.

  • The current version is v1; every path starts with /api/v1/. v1 URLs remain stable.
  • Non-breaking changes — new optional parameters, new response fields, new endpoints — may ship at any time. Clients must tolerate unknown response fields.
  • Breaking changes — removing or renaming fields, changing defaults, types, enums, error codes, or status codes — would ship under a new version path, with v1 kept working through a published migration window.
  • Deprecations and migrations are announced in the changelog before they take effect.