Ir para o conteúdo

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

POST /v1/batches
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"]
  }'
A resposta
{
  "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: 0

O 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

GET /v1/batches/{id}
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

webhook
{
  "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.

Conferindo uma entrega
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

Enviar, esperar, ler
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.