Screenshot API for Fintech: Capturing Financial Dashboards, Charts, and Compliance Evidence

2026-05-14 | Tags: [use-case, fintech, finance, compliance, charts, screenshot-api, python]

Financial applications present a specific rendering challenge: charts, candlesticks, portfolio graphs, and real-time price feeds are all rendered asynchronously via JavaScript. A raw HTTP request captures a loading skeleton; the Screenshot API's wait parameter lets the full visualization render before capture.

This matters for three fintech use cases: report generation (capturing charts for client-facing PDFs), compliance archiving (preserving the state of a portfolio or price at a specific moment), and market data dashboards (automated capture for internal analytics).

Core Financial Chart Capture

import requests
import os
from datetime import datetime, timezone

HERMES_API_KEY = os.environ["HERMES_API_KEY"]


def capture_financial_chart(
    url:         str,
    wait_ms:     int  = 5000,
    width:       int  = 1440,
    full_page:   bool = False,
    clip_height: int  = 800,
) -> bytes:
    """
    Capture a financial chart or dashboard.

    wait_ms: 5000ms recommended for financial charts.
    Candlestick charts, D3.js visualizations, and TradingView widgets
    all render asynchronously and require longer waits than typical web pages.
    full_page=False + clip_height: captures above-the-fold chart area only,
    avoiding footers/disclaimers in client-facing exports.
    """
    resp = requests.get(
        "https://hermesforge.dev/api/screenshot",
        headers={"X-API-Key": HERMES_API_KEY},
        params={
            "url":         url,
            "format":      "png",   # Lossless for financial data
            "width":       width,
            "full_page":   full_page,
            "clip_height": clip_height if not full_page else None,
            "wait":        wait_ms,
        },
        timeout=120,
    )
    resp.raise_for_status()
    return resp.content

Client Portfolio Report Generation

Capture portfolio performance charts for monthly client PDF reports:

from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.platypus import SimpleDocTemplate, Image as RLImage, Paragraph, Spacer, HRFlowable
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors
from PIL import Image
import io
import hashlib
from pathlib import Path


def generate_portfolio_report(
    client_id:   str,
    report_date: str,
    charts:      list[dict],
    output_path: str,
) -> str:
    """
    Generate a PDF portfolio report with captured chart screenshots.

    charts: list of dicts with keys: url, title, subtitle (optional), wait_ms (optional)
    """
    styles = getSampleStyleSheet()
    doc    = SimpleDocTemplate(
        output_path,
        pagesize=A4,
        leftMargin=2 * cm,
        rightMargin=2 * cm,
        topMargin=2 * cm,
        bottomMargin=2 * cm,
    )
    A4_W   = A4[0] - 4 * cm
    elements = []

    elements.append(Paragraph(f"Portfolio Report — {report_date}", styles["Heading1"]))
    elements.append(Paragraph(f"Client reference: {client_id}", styles["Normal"]))
    elements.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey))
    elements.append(Spacer(1, 0.5 * cm))

    for chart in charts:
        # Capture the chart
        img_bytes = capture_financial_chart(
            url=chart["url"],
            wait_ms=chart.get("wait_ms", 5000),
            width=1440,
            full_page=False,
            clip_height=600,
        )

        # Scale to page width
        pil_img = Image.open(io.BytesIO(img_bytes))
        w, h    = pil_img.size
        scale   = A4_W / w

        elements.append(Paragraph(chart["title"], styles["Heading2"]))
        if chart.get("subtitle"):
            elements.append(Paragraph(chart["subtitle"], styles["Normal"]))
        elements.append(Spacer(1, 0.3 * cm))
        elements.append(RLImage(io.BytesIO(img_bytes), width=A4_W, height=h * scale))
        elements.append(Spacer(1, 1 * cm))

    doc.build(elements)
    return output_path


# Example: monthly report for a client
report = generate_portfolio_report(
    client_id="CLIENT-2841",
    report_date="June 2026",
    charts=[
        {
            "title":    "Portfolio Performance YTD",
            "subtitle": "vs. benchmark (S&P 500)",
            "url":      "https://dashboard.yourbrokerage.com/client/2841/ytd",
            "wait_ms":  6000,
        },
        {
            "title":    "Asset Allocation",
            "url":      "https://dashboard.yourbrokerage.com/client/2841/allocation",
            "wait_ms":  4000,
        },
        {
            "title":    "Monthly Returns",
            "url":      "https://dashboard.yourbrokerage.com/client/2841/monthly",
            "wait_ms":  4000,
        },
    ],
    output_path=f"/reports/portfolio-CLIENT-2841-2026-06.pdf",
)

Compliance: Point-in-Time Price Evidence

Capture price screens and portfolio states for regulatory audit trails:

import boto3
import json

s3              = boto3.client("s3")
COMPLIANCE_BUCKET = os.environ["COMPLIANCE_BUCKET"]


def capture_price_evidence(
    ticker:         str,
    price_page_url: str,
    trade_id:       str,
    reason:         str,
) -> dict:
    """
    Capture price screen at moment of trade for compliance evidence.
    Stores in S3 with trade metadata. SHA-256 provides integrity proof.
    """
    captured_at = datetime.now(timezone.utc)
    img_bytes   = capture_financial_chart(price_page_url, wait_ms=4000)
    sha256      = hashlib.sha256(img_bytes).hexdigest()

    key = (
        f"compliance/price-evidence/"
        f"{captured_at.strftime('%Y/%m/%d')}/"
        f"{trade_id}-{ticker}.png"
    )

    s3.put_object(
        Bucket=COMPLIANCE_BUCKET,
        Key=key,
        Body=img_bytes,
        ContentType="image/png",
        Metadata={
            "ticker":       ticker,
            "trade-id":     trade_id,
            "captured-at":  captured_at.isoformat(),
            "sha256":       sha256,
            "reason":       reason,
        },
    )

    manifest = {
        "ticker":      ticker,
        "trade_id":    trade_id,
        "url":         price_page_url,
        "captured_at": captured_at.isoformat(),
        "sha256":      sha256,
        "s3_key":      key,
        "reason":      reason,
    }

    # Store manifest alongside image
    s3.put_object(
        Bucket=COMPLIANCE_BUCKET,
        Key=key.replace(".png", "-manifest.json"),
        Body=json.dumps(manifest, indent=2).encode(),
        ContentType="application/json",
    )
    return manifest


# Example: capture at execution time
evidence = capture_price_evidence(
    ticker="AAPL",
    price_page_url="https://finance.yourbrokerage.com/quote/AAPL",
    trade_id="TRD-20260627-00441",
    reason="Pre-execution price screen — MiFID II best execution evidence",
)

Market Data Dashboard Archiving

Capture internal market dashboards for end-of-day archiving and audit:

import schedule
import time

MARKET_DASHBOARDS = [
    {
        "name":    "fx-rates-eod",
        "url":     "https://internal.yourbrokerage.com/dashboards/fx-eod",
        "wait_ms": 6000,
        "desc":    "End-of-day FX rates",
    },
    {
        "name":    "equity-summary",
        "url":     "https://internal.yourbrokerage.com/dashboards/equity-summary",
        "wait_ms": 5000,
        "desc":    "Equity market summary",
    },
    {
        "name":    "credit-spreads",
        "url":     "https://internal.yourbrokerage.com/dashboards/credit-spreads",
        "wait_ms": 5000,
        "desc":    "Credit spread monitor",
    },
    {
        "name":    "risk-heatmap",
        "url":     "https://internal.yourbrokerage.com/dashboards/risk",
        "wait_ms": 7000,
        "desc":    "Portfolio risk heatmap",
    },
]


def eod_dashboard_archive():
    """Capture and archive all market dashboards at end of trading day."""
    archive_date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    results      = []

    for dashboard in MARKET_DASHBOARDS:
        try:
            img_bytes   = capture_financial_chart(
                dashboard["url"],
                wait_ms=dashboard["wait_ms"],
                full_page=True,
            )
            sha256 = hashlib.sha256(img_bytes).hexdigest()
            key    = f"dashboards/eod/{archive_date}/{dashboard['name']}.png"

            s3.put_object(
                Bucket=COMPLIANCE_BUCKET,
                Key=key,
                Body=img_bytes,
                ContentType="image/png",
                Metadata={
                    "dashboard":    dashboard["name"],
                    "captured-at":  datetime.now(timezone.utc).isoformat(),
                    "sha256":       sha256,
                    "description":  dashboard["desc"],
                },
            )
            results.append({"name": dashboard["name"], "status": "ok", "key": key})
            print(f"  Archived: {dashboard['name']}")
        except Exception as e:
            results.append({"name": dashboard["name"], "status": "error", "error": str(e)})
            print(f"  Failed: {dashboard['name']} — {e}")
        time.sleep(2)

    print(f"EOD archive: {sum(1 for r in results if r['status'] == 'ok')}/{len(results)} dashboards")
    return results


# Run at 17:00 London time (16:00 UTC in winter, 15:00 UTC in summer)
schedule.every().day.at("16:00").do(eod_dashboard_archive)

TradingView and Charting Library Wait Times

Chart type / Library Recommended wait Reason
TradingView widget 6000ms WebSocket price feed + SVG render
Highcharts 3000ms Data fetch + animation
D3.js 4000ms Data bind + transition
Recharts (React) 3500ms Hydration + data load
Chart.js 2500ms Canvas render (faster than SVG)
Plotly 4000ms WebGL or SVG depending on chart type
Grafana financial panels 5000ms Multiple API calls for OHLC data
Bloomberg-style tick charts 8000ms+ Streaming data; increase if candles missing

For charts that display real-time or streaming data, always capture during market hours when the data feed is live. Captures outside market hours may show stale prices or empty charts.

Regulatory Context Reference

Regulation Screenshot use case Retention
MiFID II (EU) Best execution evidence, pre/post-trade price screens 5 years
SEC Rule 17a-4 (US) Trade confirmation screens, account statements 6 years
GDPR Portfolio view before erasure request 5 years
DORA (EU) Incident state captures for operational resilience reports 5 years
Basel III Risk dashboard captures for capital adequacy evidence 7 years

Use PNG (lossless) for all compliance captures. Enable S3 Object Lock COMPLIANCE mode for evidence that may be subject to litigation hold.


Free API key at hermesforge.dev. 50 captures/day, no credit card required.