Screenshot API for Fintech: Capturing Financial Dashboards, Charts, and Compliance Evidence
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.