Website-Screenshots mit Python für Monitoring automatisieren
Eine Seite mit einer Anfrage als PNG erfassen, die Bytes direkt in eine Datei schreiben, und anhand des Content-Type verzweigen, statt zu raten, wann eine Aufnahme fehlschlägt. Getestet im August 2026.

Ein Visual-Regression-Check muss sehen, ob sich eine Seite geändert hat. Eine Statusseite braucht ein aktuelles Thumbnail von jeder Seite, die sie überwacht. Ein Wochenbericht muss zeigen, wie eine Seite tatsächlich aussah, nicht nur darauf verlinken. Alle drei brauchen ein Bild einer URL, keine Beschreibung davon, und /v1/screenshot nimmt eine URL und gibt ein Bild zurück. Der Teil, über den Leute beim ersten Mal stolpern: Die Antwort ist überhaupt kein JSON, was wichtig ist, weil jeder andere FastAF-Endpoint das ist.
Die Antwort besteht aus Bytes, nicht aus JSON
/v1/screenshot liefert rohe Bilddaten mit einem content-type von image/png. Es gibt keinen Umschlag, keinen results-Key, nichts, worauf du .json() aufrufen könntest. Schreib den Response-Body direkt in eine Datei:
import requests
API_KEY = "faf_live_..." # dein FastAF-Key
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"])
Das kostet 5 Credits. Eine Standardaufnahme von https://example.com kam als 1920x1080-PNG zurück, 22.318 Bytes. Ruf aus Gewohnheit resp.json() auf, und requests wirft einen Decoding-Fehler, weil es versucht, den Body als JSON-Text zu parsen, und ein paar tausend Bytes PNG-Binärdaten sind kein valides JSON. resp.content ist, was du willst, die rohen Bytes; resp.text würde dir nur dieselben Bytes zurückgeben, dekodiert als verstümmelter String, was nicht das ist, was du in eine .png-Datei schreiben willst.
Fehlschläge richtig prüfen
Eine fehlgeschlagene Anfrage kommt nicht als kaputtes Bild zurück, sie kommt als JSON mit einem error-Objekt zurück. Genau deshalb prüft der Code oben content-type, bevor er irgendetwas anderes tut, statt Erfolg anzunehmen und drei Funktionen später herauszufinden, dass screenshot.png in Wirklichkeit 40 Bytes JSON mit einer .png-Endung ist. Content-Type ist das eine Signal, das dir zuverlässig sagt, welche Form du zurückbekommen hast, verzweige also jedes Mal zuerst darauf, statt zu versuchen, ein kaputtes Bild im Nachhinein zu erkennen, etwa indem du die Byte-Anzahl prüfst oder die Datei mit einer Bildbibliothek öffnest und abfängst, welche Exception auch immer das wirft.
Für ein Monitoring-Dashboard zählt das mehr, als es aussieht: Der Fehlerfall ist genau der Moment, in dem du am meisten ein sauberes Signal brauchst, weil er normalerweise bedeutet, dass die Zielseite down ist oder einen Fehler wirft, also genau das, was du eigentlich erkennen wolltest. Eine Pipeline, die stillschweigend einen JSON-Fehler-Blob speichert, als wäre er ein PNG, zeigt dir ein kaputtes Thumbnail ohne Erklärung, statt der tatsächlichen Fehlermeldung, die direkt in der Antwort steckt.
Größe und Format
Über url hinaus nimmt der Endpoint: fullPage (true, um die gesamte scrollbare Seite statt eines Viewports zu erfassen), width und height, um das Viewport vor der Aufnahme zu setzen, wait_until (load, domcontentloaded oder networkidle, für Seiten, die erst nach dem initialen Load-Event fertig rendern), format (png, jpg oder webp), und quality (0 bis 100, für die verlustbehafteten Formate). wait_until ist der Parameter, der am meisten zählt für alles, was mit einem modernen Frontend-Framework gebaut ist: Eine Seite kann ihr load-Event feuern, bevor clientseitiges JavaScript fertig ist, den Inhalt zu zeichnen, den du eigentlich im Bild willst, also lohnt es sich bei einem Dashboard oder einer Single-Page-App meist, mit einer späteren Wait-Condition als dem Standard aufzunehmen, auf Kosten eines etwas langsameren Requests. Keiner dieser Parameter ändert die Tatsache, dass du Bytes zurückbekommst, nur was am Ende darin steckt:
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,
)
Für Visual Regression halte width, height und format über alle Läufe hinweg fest. Ein Diff zwischen zwei Aufnahmen ist nur aussagekräftig, wenn beide Bilder in derselben Größe aufgenommen wurden; ein geändertes Viewport zwischen Läufen zeigt dir einen Unterschied, der nichts mit der Seite zu tun hat.
Eine Monitoring-Schleife bauen
Ein Thumbnail-Job für eine Statusseite oder einen Bericht ist eine Schleife über URLs, die jede Antwort unter einem Namen speichert, den du später wiederfindest, und protokolliert, welche fehlgeschlagen sind, statt sie stillschweigend fallen zu lassen:
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"])
In unseren Messungen lag die mediane Antwortzeit bei 3,9 Sekunden. Das ist in Ordnung für eine Handvoll Dashboard-Kacheln nach Zeitplan; für ein paar hundert Seiten willst du die Requests nebenläufig statt nacheinander laufen lassen, weil ein sequenzieller Durchlauf über 500 URLs bei diesem Median allein schon etwa eine halbe Stunde dauern würde.
Was das kostet
| Volumen | Credits (30 Tage) | Paket, das das abdeckt | Kosten |
|---|---|---|---|
| 100 Seiten, täglich geprüft | 15.000 | Growth, $29 | ~$5,80 vom Paket |
| 500 Seiten, täglich geprüft | 75.000 | Growth, $29 | das ganze Paket |
| 500 Seiten, alle 6 Stunden geprüft | 300.000 | Scale, $99 | das ganze Paket |
Fehlgeschlagene Anfragen werden nie berechnet: Credits werden reserviert, wenn eine Anfrage startet, und zurückgegeben, wenn sie ohne Ergebnis bleibt, ein defektes Ziel leert also nicht heimlich dein Guthaben. Der Free Tier sind 500 Credits ohne Karte, 100 Screenshots, um Timing und Ausgabegröße an deinen eigenen Seiten zu prüfen, bevor du einen Zeitplan drumherum baust.