Automatiza capturas de pantalla de sitios web con Python para monitorización

Captura una página como PNG con una sola solicitud, escribe los bytes directo a un archivo, y decide según el content-type en vez de adivinar cuándo falla una captura. Probado en agosto de 2026.

Una comprobación de regresión visual necesita ver si una página cambió. Una página de estado necesita una miniatura actual de cada sitio que vigila. Un informe semanal necesita mostrar cómo se veía de verdad una página, no solo enlazarla. Los tres casos necesitan una imagen de una URL, no una descripción de una, y /v1/screenshot recibe una URL y devuelve una imagen. La parte que confunde a la gente la primera vez es que la respuesta no es JSON en absoluto, lo cual importa porque todos los demás endpoints de FastAF sí lo son.

La respuesta son bytes, no JSON

/v1/screenshot devuelve datos de imagen en bruto con un content-type de image/png. No hay envoltorio, no hay clave results, nada sobre lo que llamar a .json(). Escribe el cuerpo de la respuesta directo a un archivo:

import requests

API_KEY = "faf_live_..."  # tu clave de 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"])

Eso cuesta 5 créditos. Una captura por defecto de https://example.com volvió como un PNG de 1920x1080, 22.318 bytes. Llama a resp.json() por costumbre y requests lanza un error de decodificación, porque intenta parsear el cuerpo como texto JSON y unos pocos miles de bytes de binario PNG no son JSON válido. resp.content es lo que quieres, los bytes en bruto; resp.text simplemente te devolvería esos mismos bytes decodificados como una cadena ilegible, que no es lo que quieres escribir en un archivo .png.

Comprobar el fallo de la forma correcta

Una solicitud fallida no vuelve como una imagen rota, vuelve como JSON con un objeto error. Por eso exactamente el código de arriba comprueba content-type antes de hacer cualquier otra cosa, en vez de asumir el éxito y descubrir tres funciones después que screenshot.png en realidad son 40 bytes de JSON con extensión .png. Content-type es la única señal que te dice de forma fiable qué forma de respuesta obtuviste, así que decide en base a eso primero, siempre, en vez de intentar detectar una imagen mala después de los hechos comprobando, por ejemplo, el número de bytes o abriendo el archivo con una librería de imágenes y capturando la excepción que lance.

Para un dashboard de monitorización esto importa más de lo que parece: el caso de fallo es exactamente el momento en el que más necesitas una señal limpia, ya que normalmente significa que la página objetivo está caída o dando error, que es justo lo que intentabas detectar desde el principio. Un pipeline que guarda en silencio un blob de error JSON como si fuera un PNG te va a mostrar una miniatura rota sin ninguna explicación en vez del mensaje de error real que está ahí mismo en la respuesta.

Tamaño y formato

Más allá de url, el endpoint acepta: fullPage (true para capturar toda la página con scroll en vez de un solo viewport), width y height para fijar el viewport antes de capturar, wait_until (load, domcontentloaded, o networkidle, para páginas que terminan de renderizar después del evento de carga inicial), format (png, jpg, o webp), y quality (de 0 a 100, para los formatos con pérdida). wait_until es el que más importa para cualquier cosa construida con un framework de frontend moderno: una página puede disparar su evento load antes de que el JavaScript del cliente haya terminado de pintar el contenido que realmente quieres en la imagen, así que normalmente merece la pena capturar un dashboard o una single-page app con una condición de espera más tardía que la de por defecto, al coste de una solicitud algo más lenta. Ninguno de estos parámetros cambia el hecho de que recibes bytes de vuelta, solo lo que acaba dentro de ellos:

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 regresión visual, mantén width, height, y format fijos entre ejecuciones. Una comparación entre dos capturas solo tiene sentido si las dos imágenes se tomaron con el mismo tamaño; cambiar el viewport entre ejecuciones te va a mostrar una diferencia que no tiene nada que ver con la página.

Construir un bucle de monitorización

Un job de miniaturas para una página de estado o un informe es un bucle sobre URLs, que guarda cada respuesta bajo un nombre que puedas encontrar después y registra cuáles fallaron en vez de descartarlas en silencio:

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"])

En nuestras mediciones, la respuesta mediana fue de 3,9 segundos. Eso está bien para un puñado de tarjetas de dashboard con un horario; para unos cientos de páginas vas a querer que las solicitudes corran en paralelo en vez de una tras otra, ya que una pasada secuencial sobre 500 URLs a esa mediana tardaría alrededor de media hora ella sola.

Cuánto cuesta esto

Volumen Créditos (30 días) Pack que lo cubre Coste
100 páginas, comprobadas a diario 15.000 Growth, $29 ~$5,80 del pack
500 páginas, comprobadas a diario 75.000 Growth, $29 el pack entero
500 páginas, comprobadas cada 6 horas 300.000 Scale, $99 el pack entero

Las solicitudes fallidas nunca se cobran: los créditos se reservan al iniciar una solicitud y se devuelven si no llega ningún resultado, así que un objetivo roto no te vacía el saldo en silencio. El plan gratuito es de 500 créditos sin tarjeta, 100 capturas para comprobar tiempos y tamaño de salida contra tus propias páginas antes de montar un horario alrededor de esto.