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

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:

Header
Authorization: Bearer $SHOTWISP_API_KEY
Keys start with sw_ 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

Screenshot a URL
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
Create a 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"}' \
  -o page.pdf
Upload an image
curl -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.
Reference retry loop (JavaScript)
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));
  }
}
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.

Budgets: rate limits, quotas, concurrency, timeouts

  • Watch x-shotwisp-ratelimit-remaining on every response and pace requests to the plan's per-minute budget instead of colliding with 429s.
  • Watch x-shotwisp-quota-remaining to see the monthly budget fall; x-shotwisp-quota-warning appears at 80, 90, and 100 percent. At quota, requests fail with 402 quota_exceeded unless 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.