Snapi
Developer Docs

API Documentation

Base URL: https://yhfdghkh6544.com/v1. All endpoints use standard HTTP. Pass parameters in the query string (GET) or JSON body (POST).

Quickstart

  1. Sign up for free, then create a key on the API Key page in the console.
  2. Pass your key in the X-Access-Key header and call the screenshot endpoint:
curl "https://yhfdghkh6544.com/v1/screenshot?url=https://example.com&full_page=true" \
  -H "X-Access-Key: $SNAPI_KEY" \
  -o screenshot.png

Authentication

Every request requires an API Key. Pass it using any of these methods:

  • Header: X-Access-Key: snp_… (recommended for server-side requests)
  • Query parameter: access_key=snp_…
  • Signed URL (for embedding URLs in web pages without exposing your secret)

Your API Key is shown in full only once, when you create it. Store it securely. If it is exposed, disable it in the console.

Screenshot API

GET/v1/screenshot
POST/v1/screenshot

Screenshot-only parameters:

ParameterTypeDefaultDescription
formatenumpngpng, jpeg, or webp.
device_scale_factornumber1Scale factor, 1–3. Set to 2 for Retina resolution, with roughly 4 times the file size.
qualityinteger80JPEG / WebP quality, 1–100.
full_pagebooleanfalseCapture the full page. Scrolls through the page to trigger lazy loading, up to a height of 20,000 pixels.
selectorstring—Capture only the first element matching this CSS selector. Cannot be used with full_page.

Parameters shared by screenshots and PDFs:

ParameterTypeDefaultDescription
urlstring—The page URL to capture, using http or https. Provide either this or html. The # fragment is preserved, so you can capture specific routes in hash-based single-page apps (such as https://app.com/#/report/1).
htmlstring—Provide HTML directly (up to 2 MB). POST only.
viewport_widthinteger1280Viewport width, 320–3840. Values below 768 use mobile rendering.
viewport_heightinteger800Viewport height, 200–3840.
wait_untilenumnetworkidleWait strategy: domcontentloaded (fastest, best for static pages), load, or networkidle (tracks all requests from navigation and captures after page load once no requests have been in flight for 500 milliseconds; waits up to 8 additional seconds, best for single-page apps).
wait_for_selectorstring—After page load, wait up to 10 seconds for an element matching this CSS selector to appear and become visible before capturing. Returns selector_not_found if it times out (not billed). Useful for single-page apps that render content using timers or animations rather than network requests, such as .chart-ready.
delayinteger0Additional delay in milliseconds after waiting, 0–10000. Useful for animations or lazy-loaded components.
block_adsbooleantrueBlock requests to ad and tracking domains.
block_cookie_bannersbooleantrueHide cookie consent banners.
dark_modebooleanfalseEmulate prefers-color-scheme: dark.
hide_selectorsstring—A list of CSS selectors for elements to hide, such as .ad,#popup.
cachebooleantrueWhether to check the cache. Set to false to force a new render. The result is still cached.
cache_ttlinteger86400Cache lifetime in seconds, 60–2592000.
response_typeenumbinarybinary returns the file directly; json returns the file URL and metadata.

Capturing single-page apps (React, Vue, etc.):Pages run JavaScript in a real browser before capture. The default networkidle waits for the page to finish fetching data. If content renders later using timers or animations, add wait_for_selector to specify an element that indicates the content is ready. If no suitable element exists, use delay to wait a little longer.

PDF API

GET/v1/pdf
POST/v1/pdf

Each PDF uses 2 credits. Before printing, Snapi scrolls through the page to trigger lazy-loaded content. In addition to the shared parameters, PDFs support:

ParameterTypeDefaultDescription
paperenuma4Paper size: a4 or letter. The page's own @page rules take precedence.
print_backgroundbooleantruePrint background colors and images.
pdf_mediaenumprintprint uses print styles; screen uses screen styles (what you see is what you get).
HTML to PDF
curl -X POST "https://yhfdghkh6544.com/v1/pdf" \
  -H "X-Access-Key: $SNAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>Quarterly Report</h1><p>Body text…</p>", "paper": "a4"}' \
  -o report.pdf

Responses

By default (response_type=binary), the file is returned directly with the appropriate Content-Type. Response headers:

  • X-Request-Id: Request ID. Include this when reporting an issue.
  • X-Snapi-Cache: HIT or MISS
  • X-Snapi-Truncated: true: The full page exceeded 20,000 pixels and was truncated.
  • X-Snapi-Format-Changed: webp: PNG output exceeded 5 MB and was automatically converted to WebP (not converted when format=png is explicitly specified).

Set response_type=json to return the file URL and metadata:

JSON Response
curl -X POST "https://yhfdghkh6544.com/v1/screenshot" \
  -H "X-Access-Key: $SNAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "response_type": "json"}'

# Returns
{
  "url": "https://yhfdghkh6544.com/i/3f/3f9c….png",
  "cache_hit": false,
  "duration_ms": 1284,
  "format": "png",
  "width": 1280,
  "height": 800,
  "request_id": "req_01j…"
}

Signed URLs

Want to use a screenshot URL directly in <img src> without exposing your API Key? Generate a signature on your server using the key's secret (available on the API Key page in the console):

Signing Rules
canonical = Sort all parameters except signature lexicographically by name,
            format each as k=urlencode(v) (RFC 3986, with spaces encoded as %20), and join them with &
signature = hex(HMAC-SHA256(secret, canonical))

A valid signature authenticates the request. Changing any parameter (such as url) invalidates the signature. The optional expires parameter (Unix seconds) is included in the signature; expired URLs return signature_expired. To refresh the cache, change an otherwise unused parameter (such as v=2) and sign the URL again. Signed URLs support GET only.

import crypto from "node:crypto";

// RFC 3986 encoding (spaces as %20) must match the server's encoding rules
const enc = (v) =>
  encodeURIComponent(v).replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());

function signedUrl(params, accessKey, secret) {
  const all = { ...params, access_key: accessKey };
  const canonical = Object.keys(all).sort().map((k) => `${k}=${enc(String(all[k]))}`).join("&");
  const signature = crypto.createHmac("sha256", secret).update(canonical).digest("hex");
  return `https://yhfdghkh6544.com/v1/screenshot?${canonical}&signature=${signature}`;
}

const src = signedUrl({ url: "https://example.com", viewport_width: 1200 }, KEY, SECRET);
// <img src={src} alt="Web page preview" />

Caching

Requests with identical parameters share a cache entry (omitting a parameter is equivalent to explicitly passing its default; authentication and cache parameters are excluded from comparison). On a cache hit, binary mode returns a 302 redirect to the file URL with Cache-Control. Browsers follow the redirect and cache the file automatically. JSON mode returns cache_hit: true.

Cache hits are not billed.Concurrent requests with identical parameters trigger only one render.

Limits & Billing

  • Only successful (2xx) requests that miss the cache are billed: 1 credit per screenshot, 2 credits per PDF.
  • Timeouts, render failures, target site errors, invalid parameters, and authentication failures do not use credits.
  • Credits reset each calendar month (UTC). The free plan includes 100 credits per month. Requests return quota_exceeded once they are used up.
  • Exceeding the request rate or concurrency limit returns 429. The Retry-After response header specifies the recommended wait time in seconds.
  • Maximum render time: 30 seconds. Maximum download per page: 50 MB. Maximum output file size: 50 MB.

For each plan's limits, see Pricing.

Error Codes

All error responses use JSON:

Error Format
{
  "error": {
    "code": "render_failed",
    "message": "target responded with an error status",
    "upstream_status": 404,
    "request_id": "req_01j…"
  }
}
HTTPcodeMeaningBilled
400invalid_paramsA parameter is missing or out of range; message identifies the parameterNo
400html_too_largehtml exceeds 2 MBNo
401unauthorizedAPI Key is missing, invalid, or disabledNo
401signature_invalidSignature mismatch (parameters were changed or the secret is incorrect)No
401signature_expiredThe signed URL has passed its expires timeNo
403url_blockedAccess to the target address is not allowed (for example, private network addresses or localhost)No
408render_timeoutRendering exceeded 30 secondsNo
422render_failedThe target site returned 4xx/5xx, DNS failed, or a certificate error occurred; includes upstream_statusNo
422selector_not_foundNo element matched selector, or wait_for_selector timed outNo
422too_many_redirectsMore than 5 redirectsNo
429rate_limitedRequest rate limit exceeded; retry according to Retry-AfterNo
429concurrency_exceededConcurrency limit exceededNo
429quota_exceededMonthly credits used up (free plan)No
429too_many_batchesMore than 5 unfinished batch jobsNo
500internalInternal server error. Contact us with request_idNo

Async Batches

POST/v1/batch
GET/v1/batch/{id}

Submit 1–100 URLs and receive a job ID immediately (HTTP 202). Screenshots are processed one at a time in the background, so you don't need to keep a connection open. Poll the status endpoint or provide webhook_url to be notified when all items are complete. Authentication works like the synchronous endpoints (header or access_key parameter only; signed URLs are not supported).

ParameterTypeDefaultDescription
typeenumscreenshotscreenshot (screenshot) or pdf.
urlsstring[]—URLs to process, 1–100. If any URL is invalid, the entire request returns 400, and message identifies its position in the list.
optionsobject{}Parameters shared by all URLs. Supports the same parameters as the synchronous endpoints (such as full_page, format, viewport_width, and cache_ttl), except url, html, and response_type.
webhook_urlstring—Optional. URL to notify when all items are complete. See "Webhooks."
Submit a Batch Job
curl -X POST "https://yhfdghkh6544.com/v1/batch" \
  -H "X-Access-Key: $SNAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "screenshot",
    "urls": ["https://example.com", "https://example.org"],
    "options": { "full_page": true, "format": "webp" },
    "webhook_url": "https://your-app.com/hooks/snapi"
  }'

# Returns 202
{
  "id": "bat_01j…",
  "type": "screenshot",
  "status": "queued",
  "total": 2, "succeeded": 0, "failed": 0,
  "created_at": "2026-10-18T09:30:00Z", "completed_at": null,
  "webhook": { "url": "https://your-app.com/hooks/snapi", "status": "none", "attempts": 0 },
  "items": [
    { "index": 0, "url": "https://example.com", "status": "pending", "result": null, "error": null },
    { "index": 1, "url": "https://example.org", "status": "pending", "result": null, "error": null }
  ]
}

Check progress: The job's status moves from queued (queued) to running (processing) to completed (all items finished, whether successful or failed). Each URL has a status of pending, running, succeeded, or failed. Successful items include the file URL in result; failed items include an error code in error (the same codes as the synchronous endpoints).

Check Batch Status
curl "https://yhfdghkh6544.com/v1/batch/bat_01j…" -H "X-Access-Key: $SNAPI_KEY"

{
  "id": "bat_01j…",
  "status": "completed",
  "total": 2, "succeeded": 1, "failed": 1,
  "completed_at": "2026-10-18T09:30:12Z",
  "items": [
    {
      "index": 0, "url": "https://example.com", "status": "succeeded", "duration_ms": 1830,
      "result": { "url": "https://yhfdghkh6544.com/i/3f/3f9c….webp", "mime": "image/webp", "width": 1280, "height": 2410, "cache_hit": false },
      "error": null
    },
    {
      "index": 1, "url": "https://example.org", "status": "failed", "duration_ms": 640,
      "result": null,
      "error": { "code": "render_failed", "message": "target responded with an error status" }
    }
  ]
}
  • Billing works like the synchronous endpoints: each successful URL uses 1 credit for a screenshot or 2 credits for a PDF. Cache hits and failures do not use credits.
  • If you have no credits left when submitting, the request immediately returns quota_exceeded. If credits run out during processing, the remaining URLs fail with quota_exceeded.
  • Each account can have up to 5 unfinished batch jobs at a time. Exceeding this limit returns too_many_batches.
  • Result files expire according to options.cache_ttl (default: 1 day). Download and save them before they expire.
  • In status responses and webhook payloads, webhook.url includes only the URL through its path; the query string is omitted.

Webhooks

If you provide webhook_url, we send a POST to it when all items are complete. The JSON request body is { "event": "batch.completed", "batch": { … } }, where batch is identical to the status endpoint's response. Headers:

  • X-Snapi-Signature: t=1760779812,v1=5d3f…: t is the send time (Unix seconds), and v1 is the signature
  • X-Snapi-Event: batch.completed, X-Snapi-Delivery-Attempt: 1 (delivery attempt number)
Signing Rules
v1 = hex(HMAC-SHA256(secret, t + "." + Raw request body))
secret is the signing secret for the API Key used to submit the job (available on the API Key page in the console).
  • Return a 2xx response within 10 seconds, or the delivery is considered failed. Failed deliveries are retried after 10 seconds, 1 minute, and 5 minutes, for a maximum of 4 attempts.
  • In rare cases, you may receive duplicate notifications for the same job. Deduplicate by batch.id.
  • To verify the signature, compute it using the raw bytes received and check that t is within a few minutes of the current time to prevent replay attacks.
  • The callback URL must be a publicly accessible http(s) address and cannot point to a private network.
import crypto from "node:crypto";

// rawBody must contain the original bytes received. Do not use JSON.parse and then reserialize.
function verifySnapiWebhook(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // Replay protection
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const got = Buffer.from(parts.v1 ?? "");
  return got.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), got);
}

// verifySnapiWebhook(req.rawBody, req.headers["x-snapi-signature"], SNAPI_SECRET)

Usage

GET/v1/usage
shell
curl "https://yhfdghkh6544.com/v1/usage" -H "X-Access-Key: $SNAPI_KEY"

{ "month": "202610", "used": 37, "limit": 100, "remaining": 63, "overage": 0, "resets_at": "2026-11-01T00:00:00Z", ... }

Health Check

GET/v1/health

No authentication required. Returns the node status and browser pool counts (total, busy, and idle).