Snapi

API 文档

基础地址 https://yhfdghkh6544.com/v1。所有接口都是标准 HTTP,参数可以放在查询串(GET)或 JSON body(POST)里。

快速开始

  1. 免费注册,在控制台的「API Key」页面创建一个 Key。
  2. 把 Key 放进 X-Access-Key 请求头,调用截图接口:
curl "https://yhfdghkh6544.com/v1/screenshot?url=https://example.com&full_page=true" \
  -H "X-Access-Key: $SNAPI_KEY" \
  -o screenshot.png

鉴权

每个请求都需要 API Key,可以用下面任一方式传递:

  • 请求头 X-Access-Key: snp_…(推荐,服务端调用使用)
  • 查询参数 access_key=snp_…
  • 签名 URL(需要把地址放进网页时使用,secret 不会暴露)

API Key 的明文只在创建时显示一次,请妥善保存。泄露后可以在控制台停用。

截图接口

GET/v1/screenshot
POST/v1/screenshot

截图专用参数:

参数类型默认值说明
formatenumpngpng、jpeg 或 webp。
device_scale_factornumber1缩放比 1–3。设为 2 可获得 Retina 清晰度,体积约为 4 倍。
qualityinteger80JPEG / WebP 质量,1–100。
full_pagebooleanfalse截取整页。会逐屏滚动触发懒加载,高度上限 20,000 像素。
selectorstring—只截取匹配该 CSS 选择器的第一个元素。不能与 full_page 同时使用。

截图和 PDF 共用的参数:

参数类型默认值说明
urlstring—要截取的网页地址,http 或 https。与 html 二选一。
htmlstring—直接传入 HTML(最大 2 MB),仅支持 POST。
viewport_widthinteger1280视口宽度,320–3840。小于 768 时按移动设备渲染。
viewport_heightinteger800视口高度,200–3840。
wait_untilenumnetworkidle等待策略:domcontentloaded(最快,适合静态页)、load、networkidle(连续 500 毫秒无新请求,最多再等 8 秒,适合 SPA)。
delayinteger0等待完成后再延迟的毫秒数,0–10000。用于等动画或懒加载组件。
block_adsbooleantrue拦截广告与跟踪域名的请求。
block_cookie_bannersbooleantrue隐藏 Cookie 同意弹窗。
dark_modebooleanfalse模拟 prefers-color-scheme: dark。
hide_selectorsstring—要隐藏的元素,CSS 选择器列表,例如 .ad,#popup。
cachebooleantrue是否查找缓存。设为 false 时强制重新渲染,结果仍会写入缓存。
cache_ttlinteger86400缓存有效期(秒),60–2592000。
response_typeenumbinarybinary 直接返回文件;json 返回文件地址和元数据。

PDF 接口

GET/v1/pdf
POST/v1/pdf

PDF 每次计 2 次额度。打印前会先滚动一遍页面触发懒加载内容。除共用参数外,还支持:

参数类型默认值说明
paperenuma4纸张:a4 或 letter。页面自带 @page 规则时优先使用页面的设置。
print_backgroundbooleantrue打印背景色和背景图。
pdf_mediaenumprintprint 使用打印样式;screen 使用屏幕样式(所见即所得)。
HTML 转 PDF
curl -X POST "https://yhfdghkh6544.com/v1/pdf" \
  -H "X-Access-Key: $SNAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html": "<h1>季度报告</h1><p>正文……</p>", "paper": "a4"}' \
  -o report.pdf

返回结果

默认(response_type=binary)直接返回文件,Content-Type 为对应格式。响应头:

  • X-Request-Id:本次请求 ID,排查问题时请提供
  • X-Snapi-Cache:HIT 或 MISS
  • X-Snapi-Truncated: true:整页超过 20,000 像素被截断
  • X-Snapi-Format-Changed: webp:PNG 超过 5 MB 自动转为 WebP(显式指定 format=png 时不会转换)

设置 response_type=json 时返回文件地址和元数据:

JSON 返回
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"}'

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

签名 URL

想把截图地址直接写进 <img src>,又不想暴露 API Key?在你自己的服务端用 Key 对应的 secret 计算签名(secret 在控制台的 API Key 页面查看):

签名规则
canonical = 除 signature 外的全部参数,按参数名字典序排序,
            每项 k=urlencode(v)(RFC 3986,空格编码为 %20),用 & 连接
signature = hex(HMAC-SHA256(secret, canonical))

签名正确即视为已鉴权。任何参数被改动(比如换了 url),签名都会失效。可选参数 expires(Unix 秒)参与签名,过期后返回 signature_expired。想刷新缓存时,改一个无意义的参数(如 v=2)重新签名即可。签名 URL 只支持 GET。

import crypto from "node:crypto";

// RFC 3986 编码(空格为 %20),必须和服务端规则一致
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="网页预览" />

缓存

参数完全相同的请求会命中缓存(省略参数和显式传默认值视为相同;鉴权参数和缓存参数不参与比较)。命中时二进制模式返回 302 跳转到文件地址,并带 Cache-Control,浏览器会自动跟随跳转并缓存;JSON 模式返回 cache_hit: true。

命中缓存不计费。同一参数的并发请求只会渲染一次。

限额与计费

  • 只对成功(2xx)且未命中缓存的请求计费:截图 1 次,PDF 2 次。
  • 超时、渲染失败、目标站报错、参数错误、鉴权失败都不扣额度。
  • 额度按自然月(UTC)重置。免费套餐每月 100 次,用完后返回 quota_exceeded。
  • 超过每秒请求数或并发数返回 429,响应头 Retry-After 给出建议的等待秒数。
  • 单次渲染最长 30 秒;单页下载量上限 50 MB;输出文件上限 50 MB。

查看各套餐的具体限额:价格。

错误码

错误响应统一为 JSON:

错误格式
{
  "error": {
    "code": "render_failed",
    "message": "target responded with an error status",
    "upstream_status": 404,
    "request_id": "req_01j…"
  }
}
HTTPcode含义计费
400invalid_params参数缺失或超出范围,message 说明具体是哪个参数否
400html_too_largehtml 超过 2 MB否
401unauthorized缺少 API Key,或 Key 无效 / 已停用否
401signature_invalid签名不匹配(参数被改动或 secret 错误)否
401signature_expired签名 URL 已超过 expires 时间否
403url_blocked目标地址不允许访问(例如内网地址、localhost)否
408render_timeout渲染超过 30 秒否
422render_failed目标站返回 4xx/5xx、DNS 失败或证书错误;带 upstream_status否
422selector_not_foundselector 没有匹配到元素否
422too_many_redirects重定向超过 5 次否
429rate_limited超过每秒请求数,按 Retry-After 重试否
429concurrency_exceeded超过并发数否
429quota_exceeded当月额度已用完(免费套餐)否
429too_many_batches同时未完成的批量任务超过 5 个否
500internal服务端错误,请带上 request_id 联系我们否

异步批量

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

一次提交 1–100 个网址,接口立即返回任务 ID(HTTP 202),截图在后台逐个完成。适合批量截图、不想一直等待连接的场景。可以轮询查询接口,也可以填 webhook_url,全部完成后我们会主动通知你。鉴权方式和同步接口相同(只支持请求头或 access_key 参数,不支持签名 URL)。

参数类型默认值说明
typeenumscreenshotscreenshot(截图)或 pdf。
urlsstring[]—要处理的网址,1–100 个。任何一个不合法时整个请求返回 400,message 指出是第几个。
optionsobject{}所有网址共用的参数,和同步接口的参数相同(如 full_page、format、viewport_width、cache_ttl),不能包含 url、html、response_type。
webhook_urlstring—可选。全部完成后通知的地址,见「完成回调」。
提交批量任务
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"
  }'

# 返回 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 }
  ]
}

查询进度:任务状态 status 依次是 queued(排队中)、running(处理中)、completed(全部完成,无论成功还是失败)。每个网址的状态是 pending、running、succeeded 或 failed;成功的在 result 里给出文件地址,失败的在 error 里给出错误码(和同步接口的错误码一致)。

查询批量任务
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" }
    }
  ]
}
  • 计费规则和同步接口相同:每个成功的网址截图计 1 次、PDF 计 2 次,命中缓存和失败的不扣额度。
  • 提交时额度已经用完会直接返回 quota_exceeded;处理过程中额度用完,剩下的网址会以 quota_exceeded 失败。
  • 每个账号同时最多 5 个未完成的批量任务,超过返回 too_many_batches。
  • 结果文件的有效期由 options.cache_ttl 决定(默认 1 天),请及时下载保存。
  • 查询结果和回调里的 webhook.url 只显示到路径,不回显查询串。

完成回调

填了 webhook_url 时,任务全部完成后我们会向它发送一个 POST,请求体是 JSON:{ "event": "batch.completed", "batch": { … } },其中 batch 和查询接口的返回完全相同。请求头:

  • X-Snapi-Signature: t=1760779812,v1=5d3f…:t 是发送时间(Unix 秒),v1 是签名
  • X-Snapi-Event: batch.completed、X-Snapi-Delivery-Attempt: 1(第几次投递)
签名规则
v1 = hex(HMAC-SHA256(secret, t + "." + 原始请求体))
secret 是提交任务时所用 API Key 对应的签名 secret(控制台 API Key 页面查看)
  • 请在 10 秒内返回 2xx,否则视为失败。失败后会在 10 秒、1 分钟、5 分钟后各重试一次(共最多 4 次)。
  • 极少数情况下同一个任务可能收到重复通知,请按 batch.id 去重。
  • 验签时用收到的原始字节计算,并检查 t 与当前时间相差不超过几分钟,防止重放。
  • 回调地址必须是公网可访问的 http(s) 地址,不能指向内网。
import crypto from "node:crypto";

// rawBody 必须是收到的原始字节,不能先 JSON.parse 再序列化
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; // 防重放
  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)

用量查询

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", ... }

健康检查

GET/v1/health

无需鉴权,返回节点状态和浏览器池状态(总数、忙碌、空闲)。