Lotes
Mande a lista de URLs e pare de segurar uma conexão aberta. Cada uma é lida nas mesmas condições de um scrape avulso, e você escolhe entre acompanhar ou ser avisado.
Enviar a lista
curl -X POST https://api.scrapa.app/v1/batches \
-H "authorization: Bearer sc_live_your_key" \
-H "content-type: application/json" \
-d '{
"urls": ["https://example.com/a", "https://example.com/b"],
"formats": ["markdown"]
}'{
"data": {
"id": "b7c1e0a4-5f2d-4c8b-9e13-2a6d4f8c1b90",
"status": "queued",
"total": 2,
"completed": 0,
"failed": 0,
"skipped": 0,
"pool": "residential",
"cost": 0,
"expiresAt": "2026-09-07T09:12:00.000Z"
}
}
HTTP/1.1 202 Accepted
x-request-cost: 0O que volta é um id, não as páginas. É isso que o 202 quer dizer aqui. Toda URL é conferida antes de qualquer coisa ser gravada, então uma URL ruim no meio de cinco mil vira um 400 que você corrige agora, e não uma surpresa quarenta minutos depois.
Acompanhar
curl "https://api.scrapa.app/v1/batches/b7c1e0a4-5f2d-4c8b-9e13-2a6d4f8c1b90" \
-H "authorization: Bearer sc_live_your_key"
# …and once it has finished, the pages themselves:
curl "https://api.scrapa.app/v1/batches/b7c1e0a4…/?items=true&from=0" \
-H "authorization: Bearer sc_live_your_key"Consultar um lote é grátis e diz até onde ele chegou. Peça items quando quiser as páginas de verdade: vêm até duzentas por vez e, como as posições são sequenciais, a próxima página é from mais quantas você acabou de receber.
O completed conta toda URL que chegou ao fim, e é ele que você compara com o total. O failed e o skipped estão dentro dele, e não são a mesma coisa.
Ou deixe a gente avisar
{
"urls": ["https://example.com/a", "https://example.com/b"],
"webhook": {
"url": "https://your-app.example.com/hooks/scrapa",
"events": ["batch.completed"]
}
}Passe um webhook e a gente manda um POST quando o lote terminar. O segredo de assinatura volta uma única vez, na criação. Quem gera é a gente, não você, então nenhuma credencial sua passa pela rede até aqui, e perder o segredo significa criar outro lote. Peça também batch.item.completed quando um lote longo precisar ser útil antes de acabar.
import { createHmac, timingSafeEqual } from "node:crypto";
// A delivery signed longer ago than this is refused, so nobody who
// copies one on its way to you can send it to you again later.
const MAX_AGE_SECONDS = 300;
export function verify(secret: string, rawBody: string, header: string): boolean {
const t = /(?:^|,)t=(\d+)(?:,|$)/.exec(header)?.[1];
const v1 = /(?:^|,)v1=([0-9a-f]+)(?:,|$)/.exec(header)?.[1];
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > MAX_AGE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(v1, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}O cabeçalho scrapa-signature vem como t=<unix>,v1=<hmac>: HMAC-SHA256 sobre o timestamp, um ponto e o corpo cru exatamente como chegou. Compare em tempo constante e rejeite entrega assinada há mais de cinco minutos, como no exemplo, para ninguém conseguir reenviar uma entrega antiga para você.
A gente entrega só nas portas 80 e 443, e não segue redirecionamento. Um redirecionamento é um segundo host que ninguém conferiu.
Quanto custa
Nada no envio, e nada fica reservado. Cada URL custa exatamente o que a mesma chamada custaria sozinha, e as páginas que a web bloqueou também não custam nada aqui, a não ser que um script seu de browser.actions tenha rodado nelas antes. O cost do lote é o que ele já gastou, e cresce enquanto ele roda. Consulte o preço antes se quiser saber o teto.
Como nada fica reservado, um lote pode durar mais que os créditos que o começaram. As URLs que a gente não chegou a ler ficam skipped, não failed: isso não diz nada sobre a URL, e você pode reenviar exatamente essas posições. O mesmo vale para um lote que ainda está rodando 24 horas depois do envio: os resultados já não podem ser lidos, então a gente para em vez de cobrar por páginas que ninguém vai ver.
Em TypeScript
import { Scrapa } from "@scrapa/sdk";
const scrapa = new Scrapa(process.env.SCRAPA_API_KEY!);
const batch = await scrapa.batches.create({
urls: pages.map((p) => p.url),
formats: ["markdown"],
});
// Poll until every URL has a verdict, then read them in pages.
let status = await scrapa.batches.get({ id: batch.id });
while (status.completed < status.total) {
await new Promise((r) => setTimeout(r, 5_000));
status = await scrapa.batches.get({ id: batch.id });
}
for (let from = 0; from < status.total; ) {
const page = await scrapa.batches.get({ id: batch.id, items: true, from });
for (const item of page.items ?? []) {
if (item.status === "done") save(item.url, item.result!.markdown);
else if (item.status === "failed") retryLater(item.url, item.error!.code);
}
from += page.items?.length ?? status.total;
}Nada aqui é específico de lote além da espera: o resultado de um item é o mesmo objeto que o scrape() devolve, e o erro de um item é o mesmo código, então o que você já escreveu para uma página serve para cinco mil.