Guide
Using Shotwisp with AI agents
Shotwisp can be called by any AI agent, coding assistant, or autonomous workflow capable of authenticated HTTPS requests. There is no SDK requirement and no browser to drive: plain POST requests with JSON or multipart bodies, bearer-token authentication, and one predictable error envelope.
Machine-readable discovery
- https://shotwisp.com/openapi.json — OpenAPI 3.1: every endpoint, parameter, default, enum, limit, and error code. Generated from the same schemas the API validates with. Also at
/.well-known/openapi.json. - https://shotwisp.com/llms.txt — concise index for LLMs; /llms-full.txt is the complete documentation as one plain-text file.
- https://shotwisp.com/docs — human-readable reference, fully server-rendered (no JavaScript required to read it), with per-endpoint pages under
/docs/api/<endpoint>. - https://shotwisp.com/pricing — plans, quotas, and rate limits. https://shotwisp.com/changelog — dated API changes.
Authentication
A human creates an API key once at https://shotwisp.com/dashboard/api-keys (sign-up and email verification required; the Free plan is $0 and includes 250 renders and 50 uploads per month). Store it as SHOTWISP_API_KEY and send it on every request:
Authorization: Bearer $SHOTWISP_API_KEYsw_ and are shown once, at creation. All keys on an account share one rate limit and one monthly quota, so an agent fleet using several keys still draws from the same budget.Minimal working examples
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.pngcurl -X POST https://shotwisp.com/api/v1/pdf \
-H "Authorization: Bearer $SHOTWISP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' \
-o page.pdfcurl -X POST https://shotwisp.com/api/v1/upload \
-H "Authorization: Bearer $SHOTWISP_API_KEY" \
-F "file=@./image.png"Set "response": "json" on screenshot and PDF requests to receive metadata plus a hosted result URL instead of raw bytes — usually the better fit for agents that pass results along rather than store files. Per-endpoint details: /upload · /upload/batch · /screenshot · /pdf
Error handling rules
Every error is {"error": {"code", "message"}} with a stable code. Three rules cover all of them:
- Do not retry validation or authentication errors without changing the request. 401, 402, 405, 413, 415, and 422 will fail identically every time; fix the key, the quota, or the request first.
- Respect Retry-After when receiving HTTP 429. Wait the stated seconds, then retry with exponential backoff and jitter. Rejected requests are never metered or billed.
- Use exponential backoff for transient 5xx failures. 500 is safe to retry. For 502 (
capture_failed/render_failed), retry at most twice — if it persists, the target page cannot be captured; more retries only burn time.
async function callShotwisp(request, { maxAttempts = 4 } = {}) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(request.url, request.init);
if (res.ok) return res;
const { error } = await res.clone().json().catch(() => ({ error: null }));
// Never retry client errors without changing the request.
if ([400, 401, 402, 405, 413, 415, 422].includes(res.status)) {
throw new Error(`${error?.code ?? res.status}: ${error?.message ?? "request rejected"}`);
}
if (attempt >= maxAttempts) return res;
if (res.status === 429) {
// Honor Retry-After exactly, then add jittered exponential backoff.
const retryAfter = Number(res.headers.get("retry-after")) || 1;
const waitMs = Math.max(retryAfter * 1000, 2 ** attempt * 500) + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, waitMs));
continue;
}
// 500/502: transient — exponential backoff. Persistent 502 means the
// target page cannot be captured; maxAttempts stops the loop.
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
}
}| 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. |
Budgets: rate limits, quotas, concurrency, timeouts
- Watch
x-shotwisp-ratelimit-remainingon every response and pace requests to the plan's per-minute budget instead of colliding with 429s. - Watch
x-shotwisp-quota-remainingto see the monthly budget fall;x-shotwisp-quota-warningappears at 80, 90, and 100 percent. At quota, requests fail with 402quota_exceededunless overages are enabled in billing. - Keep parallel captures at or below the plan's concurrency cap (1 on Free, up to 12 on Business) or requests return 429
concurrency_limited. - Give capture requests a 90-second client timeout: page navigation gets 25s and the full render up to 60s (screenshots) or 75s (PDFs). Slow pages fail with 502 rather than hanging.
- Failed captures are logged but never metered — a 502 does not consume quota.
Things agents get wrong
- Invalid URLs: targets must be public http(s) addresses. Localhost, private networks, and cloud metadata hosts are rejected with 422 — there is no way to capture an internal page.
- Authenticated pages: custom headers and cookies are not supported, so pages behind a login cannot be captured. Check the capability matrix before promising a feature.
- Duplicate work: idempotency keys are not supported. If a request times out client-side, the capture may still have completed and been metered — verify via quota headers or the dashboard before re-issuing expensive batches.
- Wrong media type: screenshot and PDF responses are raw bytes by default. Ask for
"response": "json"if you expect JSON.