Reference
API docs
Base URL: https://shot-api.com. Send a public URL or HTML to POST /api/v1/render and get PNG, JPEG, or PDF back. ShotAPI Pro is $19/month (10,000 renders; demo is 100/month).
Auth
Customer routes accept either header. The value is the raw secret, with no extra prefix on X-API-Key:
Authorization: Bearer <key>X-API-Key: <key>
A missing key returns 401 unauthorized. The server stores a SHA-256 hash and shows a prefix (the first 16 characters) on later reads. The sample secret sk_demo_local_dev_only works only while demo mode is on. On Vercel, demo mode is off unless SHOTAPI_DEMO=1. Mint a real key with the admin token below.
POST /api/v1/keys uses SHOTAPI_ADMIN_TOKEN in those same headers. That token is not a customer API key. Browser callers may POST cross-origin; CORS allows Authorization, Content-Type, and X-API-Key.
Failures are JSON: { "error": { "code", "message", "requestId" } }, with x-request-id on the response.
POST /api/v1/render
One route covers both. format: "png" or "jpeg" returns a screenshot (image/png or image/jpeg). format: "pdf" returns application/pdf. Send exactly one of url or html. jpg is accepted and stored as JPEG.
| Field | Default | Notes |
|---|---|---|
url | Public http(s) URL, max 2048 characters. Credentials, localhost, and private networks are rejected. | |
html | Document loaded with setContent. Default max 500 KB (SHOTAPI_MAX_HTML_BYTES). | |
format | png | png, jpeg, jpg, or pdf. |
viewport.width | 1280 | 320–1920. |
viewport.height | 800 | 240–4000. |
viewport.deviceScaleFactor | 1 | 1–2. |
fullPage | false | PNG and JPEG only. PDF uses the paper size below. |
quality | 80 | JPEG quality, 1–100. Ignored for PNG and PDF. |
waitUntil | load | load, domcontentloaded, or networkidle. |
delayMs | 0 | Extra wait after load, max 5000. |
pdf.format | A4 | A4, Letter, or Legal. PDF is rendered with print media. |
pdf.landscape | false | |
pdf.printBackground | true |
Success headers: content-type, content-disposition (inline; filename="shot.png", shot.jpg, or shot.pdf), x-shotapi-usage, x-shotapi-limit, x-shotapi-plan, x-shotapi-storage, and x-request-id. A target page that returns 404 is still rendered. The API does not forward cookies or your API key. The browser user agent is ShotAPI/0.1. Request bodies must be JSON, up to 1 MB by default (SHOTAPI_MAX_BODY_BYTES).
curl -sS -X POST https://shot-api.com/api/v1/render \
-H "authorization: Bearer sk_demo_local_dev_only" \
-H "content-type: application/json" \
-o shot.png \
-d '{"html":"<h1>Hello</h1>","format":"png","viewport":{"width":1280,"height":720}}'curl -sS -X POST https://shot-api.com/api/v1/render \
-H "authorization: Bearer sk_demo_local_dev_only" \
-H "content-type: application/json" \
-o page.pdf \
-d '{"url":"https://example.com","format":"pdf"}'Replace sk_demo_local_dev_only with a key from POST /api/v1/keys when demo mode is off.
GET /api/v1/usage
Requires a customer key. Returns key (id, name, prefix, plan), period (UTC YYYY-MM), used, limit, remaining, bytesOut, storage (file, memory, or upstash), persistent, and recent (up to 10 renders: source kind, format, status, bytes, duration). Target URLs and HTML are not stored.
curl -sS https://shot-api.com/api/v1/usage \
-H "x-api-key: sk_demo_local_dev_only"POST /api/v1/keys
Mints a key. Requires SHOTAPI_ADMIN_TOKEN. Returns 201 with key, id, prefix, name, and plan. The full secret is in this response only. Body fields are optional: { "name": "acme", "plan": "demo" }. name defaults to API key (1–80 characters). plan is demo or pro, default demo. If the admin token is unset the route returns 503 admin_disabled.
curl -sS -X POST https://shot-api.com/api/v1/keys \
-H "authorization: Bearer $SHOTAPI_ADMIN_TOKEN" \
-H "content-type: application/json" \
-d '{"name":"acme","plan":"demo"}'POST /api/v1/demo/render
Open HTML preview used by the landing page. It exists only when demo mode is on (SHOTAPI_DEMO=1, or any process that is not running on Vercel). It accepts html only, up to 20 KB, and is limited to 30 requests a minute per instance. Over the limit it returns 429 rate_limited and Retry-After: 30. The render still counts against the seeded demo key. When demo mode is off the route returns 404 demo_disabled.
Billing and checkout
ShotAPI Pro is $19/month for 10,000 renders. Demo includes 100 renders. Quotas follow SHOTAPI_QUOTA_DEMO and SHOTAPI_QUOTA_PRO (defaults 100 and 10,000). Publish the Creem product, and Stripe's STRIPE_PRICE_ID when that sandbox is in use, at $19.
POST /api/v1/billing/checkout with a customer API key returns { "mode", "url" }. The provider is BILLING_PROVIDER when set (creem, paddle, stripe, or demo). Otherwise Creem is used when CREEM_API_KEY is set, then Stripe when STRIPE_SECRET_KEY is set, then demo.
- In demo mode,
modeisdemoandurlopens/billing/mock. Confirming that page, or POSTing{ "token" }to/api/v1/billing/demo-confirm, sets the key's plan topro. The token expires after 30 minutes. Demo confirm returns404when the billing provider is not demo. - With Creem,
modeiscreem. Checkout sendsmetadata.keyIdandrequest_idequal to the API key id. SetCREEM_TEST_MODE=1to callhttps://test-api.creem.io. Success returns to/billing/success. Point the webhook atPOST /api/v1/billing/webhook/creemand setCREEM_WEBHOOK_SECRET. The route checks thecreem-signatureHMAC.checkout.completedandsubscription.paidset the plan toproand store the Creem customer and subscription ids.subscription.canceledandsubscription.expiredreturn the key todemo. A scheduled cancellation does not change the plan. - With Stripe,
modeisstripeandurlis a subscription Checkout session forSTRIPE_PRICE_ID. Success returns to/billing/success; cancel returns to/billing/cancel. SetNEXT_PUBLIC_APP_URLso those URLs use the public origin. Point Stripe atPOST /api/v1/billing/webhookand setSTRIPE_WEBHOOK_SECRET. The webhook authenticates withStripe-Signature, not an API key.checkout.session.completedupgrades the key topro.customer.subscription.deleted, or an update whose status iscanceledorunpaid, returns it todemo. Calling that webhook before Stripe is configured returns501 billing_disabled. BILLING_PROVIDER=paddleis reserved. Checkout returns501 billing_disableduntil that provider is implemented.
curl -sS -X POST https://shot-api.com/api/v1/billing/checkout \
-H "authorization: Bearer sk_demo_local_dev_only"If STRIPE_METER_EVENT_NAME is set and the key has a Stripe customer id, each successful render also sends a Billing Meter event. The payload keys default to stripe_customer_id and value (override with STRIPE_METER_CUSTOMER_PAYLOAD_KEY and STRIPE_METER_VALUE_PAYLOAD_KEY). A meter failure is logged and does not fail the render.
Errors
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | Body failed validation, was not JSON, or included both or neither of url and html. |
| 400 | invalid_url / blocked_url | URL is not public http(s), or the page requests a private host. |
| 400 | invalid_token | Demo checkout token is missing, invalid, or expired. |
| 400 | invalid_signature | Stripe or Creem webhook signature is missing or invalid. |
| 401 | unauthorized | Missing or unknown API key, or the admin token does not match. |
| 402 | quota_exceeded | Monthly render quota is used up. |
| 404 | demo_disabled | Demo preview was called while demo mode is off. |
| 413 | payload_too_large | HTML, the request body, or the rendered file is too large. |
| 429 | rate_limited | Demo preview exceeded 30 requests in a minute. Retry-After: 30. |
| 500 | browser_missing | Chrome or serverless Chromium did not launch. |
| 500 | billing_misconfigured | The selected provider is missing a required setting, such as STRIPE_PRICE_ID, CREEM_PRODUCT_ID, or a valid BILLING_PROVIDER. |
| 501 | billing_disabled | Stripe or Creem webhook secret is unset, or Paddle checkout was requested. |
| 502 / 504 | render_failed / render_timeout | Browser error, empty file, or the render exceeded the timeout. Checkout failures use billing_failed (502). |
| 503 | admin_disabled | POST /api/v1/keys while SHOTAPI_ADMIN_TOKEN is unset. |
Rate limits and hard caps
POST /api/v1/renderhas no per-minute rate limit. The cap is the monthly quota: demo 100, Pro 10,000. Past that, the route returns 402. The quota is checked before rendering, so concurrent calls can finish slightly over the limit.POST /api/v1/demo/renderallows 30 requests per minute per instance. The counter is in memory and does not span instances. SetSHOTAPI_DEMO_RATELIMIT=0to turn it off. A 429 includesRetry-After: 30.- Default render timeout is 25 seconds (
RENDER_TIMEOUT_MS). The routemaxDurationis 60 seconds. - Platform response bodies cap at 4.5 MB. Full-page screenshots of long documents can hit that and return 413.
- Local renders use installed Chrome via
playwright-core. Vercel uses@sparticuz/chromium, which cold-starts by unpacking Chromium into/tmpand wants about 2 GB of memory. networkidlewaits until the network is quiet and can consume the whole timeout.- Subresource requests to private hosts are aborted. DNS rebinding is a residual risk.
- Chrome's sandbox is off unless
SHOTAPI_CHROME_NO_SANDBOX=0. Run the process in a container if you render untrusted pages.
GET /api/v1/health
No API key. Returns ok, driver, browser, billing (demo, creem, creem_incomplete, stripe, stripe_incomplete, paddle, or invalid), storage, persistent, and demo.