{"openapi":"3.1.1","info":{"title":"Shotwisp API","version":"1.2.0","summary":"Image uploads, website screenshots, and webpage-to-PDF rendering over plain HTTPS.","description":"Share images instantly and automate website screenshots or PDFs with one developer-friendly API. Start free with clear limits and expiring links.\n\nAuthenticate every request with `Authorization: Bearer <API key>` (keys start with `sw_`; create them at https://shotwisp.com/dashboard/api-keys). All errors use one envelope: `{\"error\": {\"code\", \"message\"}}` with stable machine-readable codes. Each response carries `x-shotwisp-ratelimit-*` headers (per-minute budget) and metered responses add `x-shotwisp-quota-*` headers (monthly quota). On HTTP 429, honor `Retry-After`, then back off exponentially; never retry 401/402/413/415/422 without changing the request. GET on any endpoint returns 405 `{\"error\": {\"code\": \"method_not_allowed\"}}`; unknown /api/ paths return 404 `not_found`. In rare infrastructure failures a 5xx can arrive without the JSON envelope (for example from the CDN during an outage) — treat any non-envelope 5xx as transient and retry with backoff.\n\nDocs: https://shotwisp.com/docs · AI agent guide: https://shotwisp.com/docs/ai-agents · Pricing and limits: https://shotwisp.com/pricing · Changelog: https://shotwisp.com/changelog","contact":{"name":"Shotwisp support","url":"https://shotwisp.com/contact"},"termsOfService":"https://shotwisp.com/terms"},"externalDocs":{"description":"Shotwisp API documentation","url":"https://shotwisp.com/docs"},"servers":[{"url":"https://shotwisp.com","description":"Production"}],"security":[{"BearerAuth":[]}],"tags":[{"name":"Upload","description":"Store images and get share links."},{"name":"Screenshot","description":"Capture public web pages as PNG, JPEG, or WebP images."},{"name":"PDF","description":"Render public web pages as PDFs."}],"paths":{"/api/v1/upload":{"post":{"operationId":"createUpload","tags":["Upload"],"summary":"Upload one image and get share, file, and delete URLs","description":"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.","externalDocs":{"description":"Human-readable reference for this endpoint","url":"https://shotwisp.com/docs/api/upload"},"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","contentEncoding":"binary","description":"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":{"type":"integer","minimum":60,"maximum":31536000,"description":"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."}}}}}},"responses":{"201":{"description":"The upload was stored.","headers":{"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}},"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type on the account's plan.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the monthly quota is spent (floors at 0).","schema":{"type":"string"}},"x-shotwisp-quota-usage-percent":{"description":"Usage this month as a whole-number percentage of quota.","schema":{"type":"string"}},"x-shotwisp-quota-warning":{"description":"80, 90, or 100. Present only once usage has crossed that threshold.","schema":{"type":"string"}},"x-shotwisp-overage":{"description":"\"true\" when the unit just consumed was past quota and billed as an overage. For batch uploads, describes the most recent stored file.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadResult"}}}},"401":{"description":"`unauthorized` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Not retryable: The API key is missing, malformed, revoked, or the account is suspended or unverified.","value":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sw_..."}}}}}}},"402":{"description":"`quota_exceeded` — 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.","headers":{"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the quota is spent (0 here).","schema":{"type":"string"}},"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"quota_exceeded":{"summary":"Not retryable: The monthly upload or render quota is used up and overages are off or unavailable on the plan.","value":{"error":{"code":"quota_exceeded","message":"Monthly screenshot quota exceeded (250/250). Upgrade your plan to continue."}}}}}}},"413":{"description":"`file_too_large` — An uploaded file exceeds the 10 MB per-file limit. Do not retry unchanged. Reduce the file below the limit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"file_too_large":{"summary":"Not retryable: An uploaded file exceeds the 10 MB per-file limit.","value":{"error":{"code":"file_too_large","message":"File too large. Limit is 10 MB."}}}}}}},"415":{"description":"`unsupported_type` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unsupported_type":{"summary":"Not retryable: The file's bytes do not match a supported image format (PNG, JPEG, GIF, WebP, SVG, AVIF).","value":{"error":{"code":"unsupported_type","message":"Unsupported image type. The file's contents do not match a supported format (image/png, image/jpeg, image/gif, image/webp, image/svg+xml, image/avif)."}}}}}}},"422":{"description":"`validation_error` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"validation_error":{"summary":"Not retryable: A request field failed validation. The message states which field and why.","value":{"error":{"code":"validation_error","message":"width: Too big: expected number to be <=3840"}}}}}}},"429":{"description":"`rate_limited` — 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.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Always honor this before any retry.","schema":{"type":"string"}},"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"Retryable: The plan's per-minute request budget is spent. Rejected requests are not metered.","value":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry in 27s."}}}}}}},"500":{"description":"`internal` — Unexpected error on Shotwisp's side. The request was not metered. Safe to retry with exponential backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"Retryable: Unexpected error on Shotwisp's side. The request was not metered.","value":{"error":{"code":"internal","message":"Something went wrong capturing the screenshot. Try again."}}}}}}}}}},"/api/v1/upload/batch":{"post":{"operationId":"createUploadBatch","tags":["Upload"],"summary":"Upload up to 10 images in one request","description":"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.","externalDocs":{"description":"Human-readable reference for this endpoint","url":"https://shotwisp.com/docs/api/upload-batch"},"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"array","maxItems":10,"items":{"type":"string","contentEncoding":"binary"},"description":"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":{"type":"integer","minimum":60,"maximum":31536000,"description":"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."}}}}}},"responses":{"201":{"description":"Per-file results in request order. 201 when all files stored; 207 when some failed.","headers":{"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}},"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type on the account's plan.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the monthly quota is spent (floors at 0).","schema":{"type":"string"}},"x-shotwisp-quota-usage-percent":{"description":"Usage this month as a whole-number percentage of quota.","schema":{"type":"string"}},"x-shotwisp-quota-warning":{"description":"80, 90, or 100. Present only once usage has crossed that threshold.","schema":{"type":"string"}},"x-shotwisp-overage":{"description":"\"true\" when the unit just consumed was past quota and billed as an overage. For batch uploads, describes the most recent stored file.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadBatchResult"}}}},"207":{"description":"Some files stored, some failed — inspect each item.","headers":{"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}},"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type on the account's plan.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the monthly quota is spent (floors at 0).","schema":{"type":"string"}},"x-shotwisp-quota-usage-percent":{"description":"Usage this month as a whole-number percentage of quota.","schema":{"type":"string"}},"x-shotwisp-quota-warning":{"description":"80, 90, or 100. Present only once usage has crossed that threshold.","schema":{"type":"string"}},"x-shotwisp-overage":{"description":"\"true\" when the unit just consumed was past quota and billed as an overage. For batch uploads, describes the most recent stored file.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadBatchResult"}}}},"401":{"description":"`unauthorized` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Not retryable: The API key is missing, malformed, revoked, or the account is suspended or unverified.","value":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sw_..."}}}}}}},"402":{"description":"`quota_exceeded` — 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. When every file in the batch fails, this status carries the UploadBatchResult body with per-item errors instead of the Error envelope.","headers":{"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the quota is spent (0 here).","schema":{"type":"string"}},"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/UploadBatchResult"}]},"examples":{"quota_exceeded":{"summary":"Not retryable: The monthly upload or render quota is used up and overages are off or unavailable on the plan.","value":{"error":{"code":"quota_exceeded","message":"Monthly screenshot quota exceeded (250/250). Upgrade your plan to continue."}}}}}}},"413":{"description":"`batch_too_large` — Combined file data in a batch exceeds 50 MB. Do not retry unchanged. Split the batch or reduce combined size. | `file_too_large` — An uploaded file exceeds the 10 MB per-file limit. Do not retry unchanged. Reduce the file below the limit. When every file in the batch fails, this status carries the UploadBatchResult body with per-item errors instead of the Error envelope.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/UploadBatchResult"}]},"examples":{"batch_too_large":{"summary":"Not retryable: Combined file data in a batch exceeds 50 MB.","value":{"error":{"code":"batch_too_large","message":"Batch too large. Combined file data must be 50 MB or less."}}},"file_too_large":{"summary":"Not retryable: An uploaded file exceeds the 10 MB per-file limit.","value":{"error":{"code":"file_too_large","message":"File too large. Limit is 10 MB."}}}}}}},"415":{"description":"`unsupported_type` — 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. When every file in the batch fails, this status carries the UploadBatchResult body with per-item errors instead of the Error envelope.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/UploadBatchResult"}]},"examples":{"unsupported_type":{"summary":"Not retryable: The file's bytes do not match a supported image format (PNG, JPEG, GIF, WebP, SVG, AVIF).","value":{"error":{"code":"unsupported_type","message":"Unsupported image type. The file's contents do not match a supported format (image/png, image/jpeg, image/gif, image/webp, image/svg+xml, image/avif)."}}}}}}},"422":{"description":"`too_many_files` — A batch contains more than 10 file parts. Do not retry unchanged. Send fewer files per batch. | `validation_error` — 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. When every file in the batch fails, this status carries the UploadBatchResult body with per-item errors instead of the Error envelope.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/UploadBatchResult"}]},"examples":{"too_many_files":{"summary":"Not retryable: A batch contains more than 10 file parts.","value":{"error":{"code":"too_many_files","message":"A batch can contain at most 10 images."}}},"validation_error":{"summary":"Not retryable: A request field failed validation. The message states which field and why.","value":{"error":{"code":"validation_error","message":"width: Too big: expected number to be <=3840"}}}}}}},"429":{"description":"`rate_limited` — 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.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Always honor this before any retry.","schema":{"type":"string"}},"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"Retryable: The plan's per-minute request budget is spent. Rejected requests are not metered.","value":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry in 27s."}}}}}}},"500":{"description":"`internal` — Unexpected error on Shotwisp's side. The request was not metered. Safe to retry with exponential backoff. When every file in the batch fails, this status carries the UploadBatchResult body with per-item errors instead of the Error envelope.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/Error"},{"$ref":"#/components/schemas/UploadBatchResult"}]},"examples":{"internal":{"summary":"Retryable: Unexpected error on Shotwisp's side. The request was not metered.","value":{"error":{"code":"internal","message":"Something went wrong capturing the screenshot. Try again."}}}}}}}}}},"/api/v1/screenshot":{"post":{"operationId":"createScreenshot","tags":["Screenshot"],"summary":"Capture a screenshot of a public web page","description":"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.","externalDocs":{"description":"Human-readable reference for this endpoint","url":"https://shotwisp.com/docs/api/screenshot"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScreenshotRequest"}}}},"responses":{"200":{"description":"The rendered image bytes, or capture metadata in JSON mode. The default is raw bytes; JSON is returned only when the request sets \"response\": \"json\". Byte responses carry the capture id in the x-shotwisp-id header.","headers":{"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}},"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type on the account's plan.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the monthly quota is spent (floors at 0).","schema":{"type":"string"}},"x-shotwisp-quota-usage-percent":{"description":"Usage this month as a whole-number percentage of quota.","schema":{"type":"string"}},"x-shotwisp-quota-warning":{"description":"80, 90, or 100. Present only once usage has crossed that threshold.","schema":{"type":"string"}},"x-shotwisp-overage":{"description":"\"true\" when the unit just consumed was past quota and billed as an overage. For batch uploads, describes the most recent stored file.","schema":{"type":"string"}},"x-shotwisp-id":{"description":"Id of the stored capture. Byte responses only; JSON mode carries it in the body.","schema":{"type":"string"}},"x-shotwisp-duration-ms":{"description":"Render duration in milliseconds. Byte responses only; JSON mode carries it in the body.","schema":{"type":"string"}}},"content":{"image/png":{},"image/jpeg":{},"image/webp":{},"application/json":{"schema":{"$ref":"#/components/schemas/ScreenshotMetadata"}}}},"401":{"description":"`unauthorized` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Not retryable: The API key is missing, malformed, revoked, or the account is suspended or unverified.","value":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sw_..."}}}}}}},"402":{"description":"`quota_exceeded` — 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.","headers":{"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the quota is spent (0 here).","schema":{"type":"string"}},"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"quota_exceeded":{"summary":"Not retryable: The monthly upload or render quota is used up and overages are off or unavailable on the plan.","value":{"error":{"code":"quota_exceeded","message":"Monthly screenshot quota exceeded (250/250). Upgrade your plan to continue."}}}}}}},"422":{"description":"`validation_error` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"validation_error":{"summary":"Not retryable: A request field failed validation. The message states which field and why.","value":{"error":{"code":"validation_error","message":"width: Too big: expected number to be <=3840"}}}}}}},"429":{"description":"`rate_limited` — 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` — 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.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Always honor this before any retry.","schema":{"type":"string"}},"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"Retryable: The plan's per-minute request budget is spent. Rejected requests are not metered.","value":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry in 27s."}}},"concurrency_limited":{"summary":"Retryable: The plan's concurrent-capture cap is fully in use by renders still in progress.","value":{"error":{"code":"concurrency_limited","message":"Too many captures in progress. The pro plan allows 6 at a time."}}}}}}},"500":{"description":"`internal` — Unexpected error on Shotwisp's side. The request was not metered. Safe to retry with exponential backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"Retryable: Unexpected error on Shotwisp's side. The request was not metered.","value":{"error":{"code":"internal","message":"Something went wrong capturing the screenshot. Try again."}}}}}}},"502":{"description":"`capture_failed` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"capture_failed":{"summary":"Retryable: The target page could not be loaded or captured (unreachable, timed out, or blocked the renderer). Failed captures are not metered.","value":{"error":{"code":"capture_failed","message":"We couldn't capture that page. Check that the URL is publicly reachable and try again."}}}}}}}}}},"/api/v1/pdf":{"post":{"operationId":"createPdf","tags":["PDF"],"summary":"Render a public web page as a PDF","description":"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.","externalDocs":{"description":"Human-readable reference for this endpoint","url":"https://shotwisp.com/docs/api/pdf"},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PdfRequest"}}}},"responses":{"200":{"description":"The PDF bytes, or render metadata in JSON mode. The default is raw bytes; JSON is returned only when the request sets \"response\": \"json\". Byte responses carry the capture id in the x-shotwisp-id header.","headers":{"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}},"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type on the account's plan.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the monthly quota is spent (floors at 0).","schema":{"type":"string"}},"x-shotwisp-quota-usage-percent":{"description":"Usage this month as a whole-number percentage of quota.","schema":{"type":"string"}},"x-shotwisp-quota-warning":{"description":"80, 90, or 100. Present only once usage has crossed that threshold.","schema":{"type":"string"}},"x-shotwisp-overage":{"description":"\"true\" when the unit just consumed was past quota and billed as an overage. For batch uploads, describes the most recent stored file.","schema":{"type":"string"}},"x-shotwisp-id":{"description":"Id of the stored capture. Byte responses only; JSON mode carries it in the body.","schema":{"type":"string"}},"x-shotwisp-duration-ms":{"description":"Render duration in milliseconds. Byte responses only; JSON mode carries it in the body.","schema":{"type":"string"}}},"content":{"application/pdf":{},"application/json":{"schema":{"$ref":"#/components/schemas/PdfMetadata"}}}},"401":{"description":"`unauthorized` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"unauthorized":{"summary":"Not retryable: The API key is missing, malformed, revoked, or the account is suspended or unverified.","value":{"error":{"code":"unauthorized","message":"Missing or malformed Authorization header. Expected: Bearer sw_..."}}}}}}},"402":{"description":"`quota_exceeded` — 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.","headers":{"x-shotwisp-quota-limit":{"description":"Monthly allowance for this unit type.","schema":{"type":"string"}},"x-shotwisp-quota-remaining":{"description":"Units left before the quota is spent (0 here).","schema":{"type":"string"}},"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"quota_exceeded":{"summary":"Not retryable: The monthly upload or render quota is used up and overages are off or unavailable on the plan.","value":{"error":{"code":"quota_exceeded","message":"Monthly screenshot quota exceeded (250/250). Upgrade your plan to continue."}}}}}}},"422":{"description":"`validation_error` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"validation_error":{"summary":"Not retryable: A request field failed validation. The message states which field and why.","value":{"error":{"code":"validation_error","message":"width: Too big: expected number to be <=3840"}}}}}}},"429":{"description":"`rate_limited` — 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` — 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.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Always honor this before any retry.","schema":{"type":"string"}},"x-shotwisp-ratelimit-limit":{"description":"Requests allowed per minute on the account's plan.","schema":{"type":"string"}},"x-shotwisp-ratelimit-remaining":{"description":"Requests left in the current one-minute window.","schema":{"type":"string"}},"x-shotwisp-ratelimit-reset":{"description":"Seconds until the window rolls over and the budget refills.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"rate_limited":{"summary":"Retryable: The plan's per-minute request budget is spent. Rejected requests are not metered.","value":{"error":{"code":"rate_limited","message":"Rate limit exceeded. Retry in 27s."}}},"concurrency_limited":{"summary":"Retryable: The plan's concurrent-capture cap is fully in use by renders still in progress.","value":{"error":{"code":"concurrency_limited","message":"Too many captures in progress. The pro plan allows 6 at a time."}}}}}}},"500":{"description":"`internal` — Unexpected error on Shotwisp's side. The request was not metered. Safe to retry with exponential backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"internal":{"summary":"Retryable: Unexpected error on Shotwisp's side. The request was not metered.","value":{"error":{"code":"internal","message":"Something went wrong capturing the screenshot. Try again."}}}}}}},"502":{"description":"`render_failed` — 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.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"examples":{"render_failed":{"summary":"Retryable: The target page could not be loaded or rendered as a PDF. Failed renders are not metered.","value":{"error":{"code":"render_failed","message":"We couldn't render that page as a PDF. Check that the URL is publicly reachable and try again."}}}}}}}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"API key in the Authorization header: `Authorization: Bearer sw_...`. Create and revoke keys at https://shotwisp.com/dashboard/api-keys. Keys are account-scoped; the rate limit and quota are shared across all keys on the account."}},"schemas":{"ScreenshotRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Absolute URL of the page to capture. Public http(s) addresses only — localhost, private hosts, and internal IP ranges are rejected."},"full_page":{"default":false,"description":"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.","type":"boolean"},"format":{"default":"png","description":"Output image encoding.","type":"string","enum":["png","jpeg","webp"]},"width":{"default":1440,"description":"Viewport width in CSS pixels.","type":"integer","minimum":320,"maximum":3840},"height":{"default":900,"description":"Viewport height in CSS pixels.","type":"integer","minimum":320,"maximum":2160},"response":{"default":"image","description":"\"image\" streams the encoded image bytes back; \"json\" returns capture metadata and a hosted image URL instead.","type":"string","enum":["image","json"]}},"required":["url"],"description":"Request body for POST /api/v1/screenshot."},"PdfRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"Absolute URL of the page to render. Public http(s) addresses only — localhost, private hosts, and internal IP ranges are rejected."},"paper_format":{"default":"letter","description":"Paper size of the generated PDF.","type":"string","enum":["letter","a4","legal"]},"landscape":{"default":false,"description":"Rotate the selected paper size to landscape orientation.","type":"boolean"},"print_background":{"default":true,"description":"Include CSS background colors and images in the PDF.","type":"boolean"},"prefer_css_page_size":{"default":false,"description":"Let the page's CSS @page size override paper_format when the page defines one.","type":"boolean"},"scale":{"default":1,"description":"Render scale applied to the page content.","type":"number","minimum":0.1,"maximum":2},"margin":{"default":{"top":0,"right":0,"bottom":0,"left":0},"description":"Page margins. Each side accepts a non-negative number (CSS pixels) or a CSS length string with px, in, cm, or mm units.","type":"object","properties":{"top":{"default":0,"description":"Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm.","anyOf":[{"type":"number","minimum":0,"maximum":2000},{"type":"string","maxLength":16,"pattern":"^(?:0|(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:px|in|cm|mm))$"}]},"right":{"default":0,"description":"Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm.","anyOf":[{"type":"number","minimum":0,"maximum":2000},{"type":"string","maxLength":16,"pattern":"^(?:0|(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:px|in|cm|mm))$"}]},"bottom":{"default":0,"description":"Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm.","anyOf":[{"type":"number","minimum":0,"maximum":2000},{"type":"string","maxLength":16,"pattern":"^(?:0|(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:px|in|cm|mm))$"}]},"left":{"default":0,"description":"Margin length: a non-negative number (CSS pixels, up to 2000) or a CSS length string using px, in, cm, or mm.","anyOf":[{"type":"number","minimum":0,"maximum":2000},{"type":"string","maxLength":16,"pattern":"^(?:0|(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:px|in|cm|mm))$"}]}}},"viewport":{"default":{"width":1440,"height":900},"description":"Browser viewport applied before printing. Controls the page's responsive layout, not the paper dimensions.","type":"object","properties":{"width":{"default":1440,"description":"Viewport width in CSS pixels.","type":"integer","minimum":320,"maximum":3840},"height":{"default":900,"description":"Viewport height in CSS pixels.","type":"integer","minimum":320,"maximum":2160}}},"wait_until":{"default":"networkidle2","description":"Page readiness event to wait for before rendering.","type":"string","enum":["load","domcontentloaded","networkidle0","networkidle2"]},"delay_ms":{"default":0,"description":"Extra wait after page readiness, in milliseconds. Useful for animations or late client-side rendering.","type":"integer","minimum":0,"maximum":10000},"response":{"default":"pdf","description":"\"pdf\" streams application/pdf bytes back; \"json\" returns render metadata and a hosted PDF URL instead.","type":"string","enum":["pdf","json"]}},"required":["url"],"description":"Request body for POST /api/v1/pdf."},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable error code.","enum":["unauthorized","quota_exceeded","method_not_allowed","not_found","file_too_large","batch_too_large","unsupported_type","validation_error","too_many_files","rate_limited","concurrency_limited","internal","capture_failed","render_failed"]},"message":{"type":"string","description":"Human-readable explanation, naming the failing field where applicable."}}}}},"UploadResult":{"type":"object","required":["id","slug","url","file_url","delete_url","expires_at","size_bytes","content_type","filename"],"properties":{"id":{"type":"string","description":"Unique upload id."},"slug":{"type":"string","description":"Short slug used in share URLs."},"url":{"type":"string","format":"uri","description":"Share page that renders the image in a viewer."},"file_url":{"type":"string","format":"uri","description":"Direct URL of the raw image bytes."},"delete_url":{"type":"string","format":"uri","description":"Tokenized URL that deletes the upload without further authentication. Treat as a secret."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the link stops resolving and the file is deleted; null when no expiry is scheduled."},"size_bytes":{"type":"integer","description":"Stored file size in bytes (after SVG sanitization)."},"content_type":{"type":"string","description":"MIME type detected from the file's bytes — not necessarily the type that was sent.","enum":["image/png","image/jpeg","image/gif","image/webp","image/svg+xml","image/avif"]},"filename":{"type":"string","description":"Sanitized stored filename."}}},"UploadBatchResult":{"type":"object","required":["items","summary"],"properties":{"items":{"type":"array","description":"One entry per submitted file, in request order.","items":{"type":"object","required":["ok","index","source_filename"],"properties":{"ok":{"type":"boolean","description":"Whether this file was stored successfully."},"index":{"type":"integer","description":"Zero-based position of the file in the request."},"source_filename":{"type":"string","description":"Filename as submitted."},"upload":{"type":"object","required":["id","slug","url","file_url","delete_url","expires_at","size_bytes","content_type","filename"],"properties":{"id":{"type":"string","description":"Unique upload id."},"slug":{"type":"string","description":"Short slug used in share URLs."},"url":{"type":"string","format":"uri","description":"Share page that renders the image in a viewer."},"file_url":{"type":"string","format":"uri","description":"Direct URL of the raw image bytes."},"delete_url":{"type":"string","format":"uri","description":"Tokenized URL that deletes the upload without further authentication. Treat as a secret."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When the link stops resolving and the file is deleted; null when no expiry is scheduled."},"size_bytes":{"type":"integer","description":"Stored file size in bytes (after SVG sanitization)."},"content_type":{"type":"string","description":"MIME type detected from the file's bytes — not necessarily the type that was sent.","enum":["image/png","image/jpeg","image/gif","image/webp","image/svg+xml","image/avif"]},"filename":{"type":"string","description":"Sanitized stored filename."}},"description":"Present when ok is true."},"error":{"type":"object","description":"Present when ok is false.","required":["code","message"],"properties":{"code":{"type":"string","enum":["file_too_large","unsupported_type","validation_error","quota_exceeded","internal"],"description":"Per-file error code."},"message":{"type":"string"}}}}}},"summary":{"type":"object","required":["total","succeeded","failed"],"properties":{"total":{"type":"integer"},"succeeded":{"type":"integer"},"failed":{"type":"integer"}}}}},"ScreenshotMetadata":{"type":"object","description":"Returned when the request sets \"response\": \"json\".","required":["id","url","format","width","height","full_page","duration_ms","size_bytes"],"properties":{"id":{"type":"string","description":"Unique capture id."},"url":{"type":"string","format":"uri","description":"Hosted URL of the stored capture (the image we took, not the page captured)."},"format":{"type":"string","enum":["png","jpeg","webp"]},"width":{"type":"integer","description":"Viewport width used."},"height":{"type":"integer","description":"Viewport height used."},"full_page":{"type":"boolean"},"duration_ms":{"type":["integer","null"],"description":"Render duration in milliseconds."},"size_bytes":{"type":["integer","null"],"description":"Encoded image size."}}},"PdfMetadata":{"type":"object","description":"Returned when the request sets \"response\": \"json\".","required":["id","url","paper_format","landscape","print_background","prefer_css_page_size","scale","margin","viewport","wait_until","delay_ms","duration_ms","size_bytes"],"properties":{"id":{"type":"string","description":"Unique render id."},"url":{"type":"string","format":"uri","description":"Hosted URL of the stored PDF."},"paper_format":{"type":"string","enum":["letter","a4","legal"]},"landscape":{"type":"boolean"},"print_background":{"type":"boolean"},"prefer_css_page_size":{"type":"boolean"},"scale":{"type":"number"},"margin":{"type":"object","properties":{"top":{"type":["number","string"]},"right":{"type":["number","string"]},"bottom":{"type":["number","string"]},"left":{"type":["number","string"]}}},"viewport":{"type":"object","properties":{"width":{"type":"integer"},"height":{"type":"integer"}}},"wait_until":{"type":"string","enum":["load","domcontentloaded","networkidle0","networkidle2"]},"delay_ms":{"type":"integer"},"duration_ms":{"type":["integer","null"]},"size_bytes":{"type":["integer","null"]}}}}},"x-error-codes":{"unauthorized":{"status":401,"retryable":false,"guidance":"Do not retry with the same key. Fix the Authorization header or create a new API key."},"quota_exceeded":{"status":402,"retryable":false,"guidance":"Do not retry until the monthly quota resets, the plan is upgraded, or overages are enabled."},"method_not_allowed":{"status":405,"retryable":false,"guidance":"Do not retry. Use POST."},"not_found":{"status":404,"retryable":false,"guidance":"Do not retry. Check the endpoint path against /openapi.json."},"file_too_large":{"status":413,"retryable":false,"guidance":"Do not retry unchanged. Reduce the file below the limit."},"batch_too_large":{"status":413,"retryable":false,"guidance":"Do not retry unchanged. Split the batch or reduce combined size."},"unsupported_type":{"status":415,"retryable":false,"guidance":"Do not retry unchanged. Convert the file to a supported image format."},"validation_error":{"status":422,"retryable":false,"guidance":"Do not retry unchanged. The message names the failing field; fix it first."},"too_many_files":{"status":422,"retryable":false,"guidance":"Do not retry unchanged. Send fewer files per batch."},"rate_limited":{"status":429,"retryable":true,"guidance":"Retry after waiting the number of seconds in the Retry-After header, then back off exponentially with jitter."},"concurrency_limited":{"status":429,"retryable":true,"guidance":"Retry after the Retry-After delay once an in-flight capture finishes. Reduce parallelism to the plan's concurrency cap."},"internal":{"status":500,"retryable":true,"guidance":"Safe to retry with exponential backoff."},"capture_failed":{"status":502,"retryable":true,"guidance":"Retry once or twice with backoff. If it persists, the target page cannot be captured — do not keep retrying."},"render_failed":{"status":502,"retryable":true,"guidance":"Retry once or twice with backoff. If it persists, the target page cannot be rendered — do not keep retrying."}}}