Automatize screenshots de sites com Python para monitoramento

Capture uma página como PNG com uma requisição, grave os bytes direto num arquivo, e decida com base no content-type em vez de adivinhar quando uma captura falha. Testado em agosto de 2026.

Uma checagem de regressão visual precisa ver se uma página mudou. Uma status page precisa de uma thumbnail atual de cada site que ela monitora. Um relatório semanal precisa mostrar como uma página realmente estava, não só linkar para ela. As três coisas precisam de uma imagem de uma URL, não uma descrição dela, e o /v1/screenshot recebe uma URL e devolve uma imagem. A parte que confunde as pessoas na primeira vez é que a resposta não é JSON de jeito nenhum, o que importa porque todo outro endpoint da FastAF é.

A resposta é bytes, não JSON

O /v1/screenshot retorna dados de imagem crus com um content-type de image/png. Não tem envelope, não tem chave results, nada para chamar .json(). Grave o corpo da resposta direto num arquivo:

import requests

API_KEY = "faf_live_..."  # sua chave FastAF

resp = requests.get(
    "https://api.fastaf.co/v1/screenshot",
    params={"url": "https://example.com"},
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=60,
)

if resp.headers.get("content-type", "").startswith("image/"):
    with open("screenshot.png", "wb") as f:
        f.write(resp.content)
else:
    print("capture failed:", resp.json()["error"])

Isso custa 5 créditos. Uma captura padrão de https://example.com voltou como um PNG de 1920x1080, 22.318 bytes. Chame resp.json() por hábito e o requests lança um erro de decodificação, porque ele tenta parsear o corpo como texto JSON e alguns milhares de bytes de binário PNG não são JSON válido. resp.content é o que você quer, os bytes crus; resp.text só te devolveria os mesmos bytes decodificados como uma string corrompida, que não é o que você quer gravar num arquivo .png.

Verificando falha do jeito certo

Uma requisição com falha não volta como uma imagem quebrada, ela volta como JSON com um objeto error. É exatamente por isso que o código acima checa o content-type antes de fazer qualquer outra coisa, em vez de assumir sucesso e descobrir três funções depois que screenshot.png na verdade são 40 bytes de JSON com extensão .png. Content-type é o único sinal que confiavelmente te diz qual formato voltou, então decida com base nisso primeiro, sempre, em vez de tentar detectar uma imagem ruim depois do fato checando, digamos, a contagem de bytes ou abrindo o arquivo com uma biblioteca de imagem e capturando qualquer exceção que isso lance.

Para um dashboard de monitoramento isso importa mais do que parece: o caso de falha é exatamente o momento em que você mais precisa de um sinal limpo, já que geralmente significa que a página alvo está fora do ar ou dando erro, que é a própria coisa que você estava tentando detectar. Um pipeline que silenciosamente salva um blob de erro JSON como se fosse um PNG vai te mostrar uma thumbnail quebrada sem explicação nenhuma, em vez da mensagem de erro de verdade que estava bem ali na resposta.

Tamanho e formato

Além de url, o endpoint recebe: fullPage (true para capturar a página inteira com scroll em vez de um viewport só), width e height para definir o viewport antes da captura, wait_until (load, domcontentloaded, ou networkidle, para páginas que terminam de renderizar depois do evento de load inicial), format (png, jpg, ou webp), e quality (0 a 100, para os formatos com perda). wait_until é o que mais importa para qualquer coisa construída com um framework de frontend moderno: uma página pode disparar o evento load antes do JavaScript do lado do cliente terminar de pintar o conteúdo que você realmente quer na imagem, então um dashboard ou single-page app geralmente vale a pena capturar com uma condição de espera mais tardia que o padrão, ao custo de uma requisição um pouco mais lenta. Nenhum desses parâmetros muda o fato de que você recebe bytes de volta, só o que acaba dentro deles:

resp = requests.get(
    "https://api.fastaf.co/v1/screenshot",
    params={
        "url": "https://example.com",
        "fullPage": "true",
        "format": "jpg",
        "quality": 80,
    },
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=60,
)

Para regressão visual, mantenha width, height, e format fixos entre execuções. Uma diferença entre duas capturas só faz sentido se as duas imagens foram tiradas no mesmo tamanho; mudar o viewport entre execuções vai te mostrar uma diferença que não tem nada a ver com a página.

Construindo um loop de monitoramento

Um job de thumbnails para uma status page ou um relatório é um loop sobre URLs, salvando cada resposta com um nome que você consegue achar depois e registrando quais falharam em vez de descartar silenciosamente:

import os

def capture_all(urls, out_dir="captures"):
    os.makedirs(out_dir, exist_ok=True)
    for i, url in enumerate(urls):
        resp = requests.get(
            "https://api.fastaf.co/v1/screenshot",
            params={"url": url, "width": 1280, "height": 800},
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=60,
        )
        if resp.headers.get("content-type", "").startswith("image/"):
            with open(f"{out_dir}/{i}.png", "wb") as f:
                f.write(resp.content)
        else:
            print(url, "failed:", resp.json()["error"])

Nas nossas medições, a resposta mediana foi 3,9 segundos. Isso é tranquilo para alguns tiles de dashboard num cronograma; para algumas centenas de páginas você vai querer as requisições rodando concorrentemente em vez de uma após a outra, já que uma passada sequencial por 500 URLs nessa mediana levaria por volta de meia hora sozinha.

Quanto isso custa

Volume Créditos (30 dias) Pacote que cobre Custo
100 páginas, checadas diariamente 15.000 Growth, $29 ~$5,80 do pacote
500 páginas, checadas diariamente 75.000 Growth, $29 o pacote inteiro
500 páginas, checadas a cada 6 horas 300.000 Scale, $99 o pacote inteiro

Requisições com falha nunca são cobradas: os créditos são reservados quando a requisição começa e devolvidos se ela não voltar com um resultado, então um alvo quebrado não drena seu saldo silenciosamente. O plano gratuito é de 500 créditos sem cartão, 100 screenshots para checar tempo de resposta e tamanho de saída contra suas próprias páginas antes de construir um cronograma em torno disso.