Resposta rápida
O que significam os códigos de erro do WebReaderAPI, e quais devo tentar de novo?
Os erros WebReaderAPI vêm em três grupos, e nenhum deles é jamais cobrado. Problemas na requisição (invalid_url, invalid_api_key, insufficient_credits) exigem uma correção do seu lado. Decisões do site (blocked_by_robots, blocked_by_site_policy, target_refused) são definitivas. Falhas temporárias (fetch_failed, fetch_timeout, concurrency_limit) podem ser tentadas de novo com backoff. Os créditos começam em US$ 0,07/1K páginas, pagos com cripto e sem KYC.
Quais códigos de erro a API pode retornar?
O corpo é sempre {"error": {"code", "message"}}, mais target_status quando a recusa veio do site.
| HTTP | code | Causa | Tentar novamente? |
|---|---|---|---|
| 400 | invalid_url | Não é http ou https, muito longo, ou um endereço privado, local ou de metadados de nuvem | Não: corrija a URL |
| 400 | invalid_parameter | Um parâmetro tem um valor fora da lista ou do intervalo | Não: corrija a requisição |
| 401 | invalid_api_key | Chave ausente, errada ou revogada | Não: verifique a chave |
| 402 | insufficient_credits | Seu saldo não tem mais créditos | Depois de comprar um pacote |
| 403 | blocked_by_robots | O robots.txt do site não permite a página para o WebReaderAPI ou para todos os robôs | Não: definitivo |
| 403 | blocked_by_site_policy | O site declara Content-Signal: ai-input=no | Não: definitivo |
| 422 | unsupported_content | Um tipo que não lemos (vídeo, arquivo) ou um arquivo grande demais | Não |
| 429 | concurrency_limit | Muitas das suas requisições estão rodando ao mesmo tempo | Sim, com backoff |
| 502 | target_refused | O site recusou: 403, 429, uma página de desafio ou uma barreira de login; o status está em target_status | Não: definitivo |
| 502 | fetch_failed | Erro de DNS, TLS ou conexão, ou o site respondeu 404 ou 410 | Uma ou duas vezes, com backoff |
| 504 | fetch_timeout | O site não respondeu dentro de timeout | Sim, com backoff ou um timeout maior |
Por que robots.txt e recusas do site são definitivos?
Porque são a decisão do dono do site, e o WebReaderAPI é feito para segui-la. Respeitamos o robots.txt: nosso crawler se identifica como WebReaderAPI, segue as regras escritas para ele ou para todos os robôs e não lê uma página que o site proíbe. Páginas recusadas não são cobradas. Se um site declara Content-Signal: ai-input=no no seu robots.txt, não lemos as páginas dele para você.
Quando um site recusa uma requisição de outra forma, com um 403, um 429, uma página de desafio ou um formulário de login, você recebe target_refused e o status do próprio site em target_status. Se um site recusa nossa requisição, devolvemos a resposta dele: nunca resolvemos CAPTCHAs, nunca entramos em contas e nunca tentamos contornar um bloqueio ou um paywall. Tentar a mesma URL de novo um minuto depois normalmente dará a mesma resposta, então trate esses códigos como um resultado, não como uma falha: registre-os e siga em frente.
Como verifico por que uma página foi recusada?
Abra o /robots.txt do site e procure um grupo que nomeie WebReaderAPI, ou o grupo * que se aplica a todos os robôs. Uma linha Disallow que corresponde ao caminho explica um blocked_by_robots. Uma linha como Content-Signal: ai-input=no explica um blocked_by_site_policy.
Mantemos uma cópia de cada robots.txt por no máximo 24 horas. Se um site acabou de abrir uma seção para robôs, a mudança pode levar até um dia para valer. Os donos de sites podem ler como nosso crawler se comporta, e como permiti-lo ou bloqueá-lo, na nossa página do crawler.
Como devo tentar novamente falhas temporárias?
Tente novamente apenas concurrency_limit, fetch_timeout e, uma ou duas vezes, fetch_failed. Espere antes de cada nova tentativa e dobre a espera a cada vez, com um pouco de jitter aleatório para que workers paralelos não tentem juntos. Pare após 4 ou 5 tentativas.
Para concurrency_limit, a melhor solução é enviar menos requisições ao mesmo tempo: Cada conta tem um limite de quantas páginas lê ao mesmo tempo; seu dashboard mostra esse limite, e o suporte pode aumentá-lo. Para fetch_timeout, aumente timeout até 60 segundos antes de tentar novamente um site lento. Tentativas não custam nada, porque uma tentativa que falha nunca é cobrada.
import os
import random
import time
import requests
API = "https://webreaderapi.com/api/v1/read"
HEADERS = {"Authorization": "Bearer " + os.environ["WEBREADER_API_KEY"]}
RETRY = {"concurrency_limit", "fetch_timeout", "fetch_failed"}
FINAL = {"blocked_by_robots", "blocked_by_site_policy", "target_refused", "unsupported_content"}
def read_page(url, attempts=5, **options):
wait = 1.0
for attempt in range(1, attempts + 1):
resp = requests.get(API, headers=HEADERS, params={"url": url, **options}, timeout=75)
if resp.status_code == 200:
return resp.json()
error = resp.json().get("error", {})
code = error.get("code", "unknown")
if code in FINAL:
return {"url": url, "skipped": code, "target_status": error.get("target_status")}
if code not in RETRY or attempt == attempts or (code == "fetch_failed" and attempt >= 3):
raise RuntimeError(f"{code}: {error.get('message')}")
time.sleep(wait + random.uniform(0, wait / 2))
wait = min(wait * 2, 30)
raise RuntimeError("unreachable")
if __name__ == "__main__":
print(read_page("https://example.com/blog/first-post", max_chars=20000).get("title"))
const RETRY = new Set(['concurrency_limit', 'fetch_timeout', 'fetch_failed']);
const FINAL = new Set(['blocked_by_robots', 'blocked_by_site_policy', 'target_refused', 'unsupported_content']);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function readPage(url, options = {}, attempts = 5) {
let wait = 1000;
for (let attempt = 1; attempt <= attempts; attempt++) {
const params = new URLSearchParams({ url, ...options });
const res = await fetch(`https://webreaderapi.com/api/v1/read?${params}`, {
headers: { Authorization: `Bearer ${process.env.WEBREADER_API_KEY}` },
});
const body = await res.json();
if (res.ok) return body;
const code = body.error?.code ?? 'unknown';
if (FINAL.has(code)) return { url, skipped: code, target_status: body.error.target_status };
if (!RETRY.has(code) || attempt === attempts) throw new Error(`${code}: ${body.error?.message}`);
await sleep(wait + Math.random() * (wait / 2));
wait = Math.min(wait * 2, 30000);
}
}
E os erros dentro de um lote ou crawl?
Um job não para porque uma página falha. Essa página aparece em results como {url, error} com os mesmos códigos acima, e não custa nada. Um rastreamento (crawl) que encontra blocked_by_robots em uma seção simplesmente lê o restante do site.
Ao final de um job, conte os códigos de erro. Muitos target_refused em um site significam que o site não aceita leitores automatizados; muitos fetch_timeout significam que ele é lento, e um timeout maior nas opções do job pode ajudar.
Erros: dúvidas frequentes
Sou cobrado por uma página que o robots.txt bloqueia?
Não. 1 crédito = 1 página lida sem navegador; 2 créditos quando a página precisa de um navegador para renderizar JavaScript. Uma requisição que falha, ou que o robots.txt de um site recusa, não é cobrada. O mesmo vale para todos os outros erros desta página.
Você consegue ler uma página que bloqueia seu crawler se eu pedir?
Não. Seguimos a escolha do dono do site, e a política é a mesma para todos os clientes. Se o site for seu, adicione uma regra para WebReaderAPI no seu robots.txt; a página do crawler mostra como.
Por que recebo fetch_failed para uma página que existe?
O site pode ter ficado fora do ar, ter demorado a aceitar uma conexão TLS ou estar retornando 404 para clientes automatizados. Tente de novo uma ou duas vezes com backoff. Se continuar falhando, verifique o endereço em um navegador.
O que meu agente deve dizer ao usuário após uma recusa?
Algo simples, como: o site não permite a leitura automatizada dessa página. Depois, deixe o modelo usar outras fontes. Retornar o código de erro como resultado da ferramenta é o suficiente para a maioria dos modelos.