api

POST /api/audits/:id/pdf und GET /api/pdfs/:id

Gebrandetes PDF aus abgeschlossenem Audit erzeugen. Zwei Schritte: POST reiht das Rendern ein, GET liefert Status oder PDF-Stream, sobald bereit.

Was diese Endpunkte machen

PDF-Erzeugung ist ein Zwei-Schritt-Flow. Zuerst das Rendern mit POST /api/audits/:id/pdf einreihen. Dann GET /api/pdfs/:id pollen, bis status === "ready" und die Datei streamen. Das Rendern dauert 5 bis 20 Sekunden, weil ein Headless-Chromium den White-Label-HTML-Report druckt.

  • POST /api/audits/:id/pdf reiht einen Render-Job ein. Body ist optional und akzeptiert language (einer aus en, es, de, fr, pt, it) und brand_id (ein gespeicherter Brand-Kit oder null für reines MetricSpot-Branding).
  • GET /api/pdfs/:id liefert standardmäßig JSON-Status. Mit Accept: application/pdf die Datei streamen, sobald status === "ready".
  • Gebrandete PDFs sind Pro-only: White-Label-Branding (Logo, Farbe, Footer) wird per Plan gegated. Schlichte MetricSpot-gebrandete PDFs sind in jedem bezahlten Plan verfügbar.
  • Der Audit muss status: "completed" sein, bevor ein PDF eingereiht werden kann. PDF für einen noch laufenden Audit liefert 409.

Warum das wichtig ist

PDFs sind das Deliverable für Agenturen, Freelancer und Berater. Ein nächtliches automatisches Rendern bedeutet, dass jeder Kunde morgens einen frischen Report im Postfach öffnet, ohne dass jemand in der Agentur auf einen Knopf gedrückt hat.

Konkrete Workflows:

  • Ein Agentur-Cron iteriert jede gespeicherte Kunden-URL, ruft POST /api/audits/:id/pdf mit der brand_id der Agentur auf, pollt bis fertig und mailt das PDF an den Kunden.
  • Ein Zapier-Zap überwacht neue Audits in GET /api/audits, triggert ein PDF-Rendering und lädt die Datei in einen mit dem Kunden geteilten Dropbox-Ordner hoch.

Wie du sie nutzt

Schritt 1: Rendern einreihen

POST /api/audits/12345/pdf HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

{ "language": "de", "brand_id": null }

Schritt 2: Status pollen

GET /api/pdfs/9876 HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx

Schritt 3: Datei herunterladen

GET /api/pdfs/9876 HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/pdf

curl (voller Flow)

TOKEN="ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"

# 1. Einreihen
PDF_ID=$(curl -s -X POST https://app.metricspot.com/api/audits/12345/pdf \
  -H "authorization: Bearer $TOKEN" \
  -H "content-type: application/json" \
  -d '{"language":"de"}' | jq -r .pdf.id)

# 2. Status pollen
while true; do
  STATUS=$(curl -s https://app.metricspot.com/api/pdfs/$PDF_ID \
    -H "authorization: Bearer $TOKEN" | jq -r .pdf.status)
  [ "$STATUS" = "ready" ] && break
  [ "$STATUS" = "failed" ] && { echo "render failed"; exit 1; }
  sleep 2
done

# 3. Download
curl -o report.pdf https://app.metricspot.com/api/pdfs/$PDF_ID \
  -H "authorization: Bearer $TOKEN" \
  -H "accept: application/pdf"

Node (einreihen + pollen + Download)

const headers = { authorization: "Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx" };

const queue = await fetch("https://app.metricspot.com/api/audits/12345/pdf", {
  method: "POST",
  headers: { ...headers, "content-type": "application/json" },
  body: JSON.stringify({ language: "de" }),
});
const { pdf } = await queue.json();

while (true) {
  await new Promise((r) => setTimeout(r, 2000));
  const r = await fetch(`https://app.metricspot.com/api/pdfs/${pdf.id}`, { headers });
  const data = await r.json();
  if (data.pdf.status === "ready") break;
  if (data.pdf.status === "failed") throw new Error(data.pdf.error_message);
}

const file = await fetch(`https://app.metricspot.com/api/pdfs/${pdf.id}`, {
  headers: { ...headers, accept: "application/pdf" },
});
const buf = await file.arrayBuffer();
await Bun.write("report.pdf", buf);

Python httpx

import httpx, time, pathlib

HEADERS = {"authorization": "Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"}

r = httpx.post(
    "https://app.metricspot.com/api/audits/12345/pdf",
    headers=HEADERS, json={"language": "de"}, timeout=30.0,
)
pdf_id = r.json()["pdf"]["id"]

while True:
    time.sleep(2)
    s = httpx.get(f"https://app.metricspot.com/api/pdfs/{pdf_id}", headers=HEADERS).json()
    if s["pdf"]["status"] == "ready":
        break
    if s["pdf"]["status"] == "failed":
        raise RuntimeError(s["pdf"]["error_message"])

resp = httpx.get(
    f"https://app.metricspot.com/api/pdfs/{pdf_id}",
    headers={**HEADERS, "accept": "application/pdf"},
    timeout=60.0,
)
pathlib.Path("report.pdf").write_bytes(resp.content)

Antwort

POST /api/audits/:id/pdf

201 Created:

{
  "pdf": {
    "id": 9876,
    "audit_id": 12345,
    "brand_id": null,
    "language": "de",
    "status": "pending",
    "created_at": "2026-05-14T10:20:01.000Z"
  }
}

GET /api/pdfs/:id (JSON-Status)

200 OK:

{
  "pdf": {
    "id": 9876,
    "audit_id": 12345,
    "status": "ready",
    "error_message": null,
    "created_at": "2026-05-14T10:20:01.000Z",
    "generated_at": "2026-05-14T10:20:18.000Z",
    "download_url": "/api/pdfs/9876/download"
  }
}

Felder:

  • status: pending (in Queue), rendering (Chromium läuft), ready oder failed.
  • error_message: gesetzt bei status === "failed".
  • generated_at: ISO-Zeitstempel, wenn die Datei fertig gerendert war, sonst null.
  • download_url: relativer Pfad zum streamenden Download-Endpunkt, nur bei ready gesetzt.

GET /api/pdfs/:id mit Accept: application/pdf

Streamt die Datei mit Content-Type: application/pdf und Content-Disposition: attachment; filename="<domain>-audit.pdf". Statuscode 200. Fehlt die Datei auf der Platte (selten, deutet GC an), kommt 410 Gone.

Fehler-Envelope

{ "error": "Audit not yet complete" }

Häufige Fehler

CodeWannAktion
UNAUTHORIZED (401)Bearer-Token fehlt oder ist ungültigSchlüssel erzeugen auf app.metricspot.com/settings/api-keys
FORBIDDEN (403)Audit oder PDF gehört einem anderen KontoSchlüssel des Besitzerkontos verwenden
AUDIT_NOT_FOUND (404)Audit-id existiert nichtEigenen Audit übergeben
INVALID_URL (400):id keine positive ganze ZahlGanzzahlige id übergeben
CONFLICT (409)Audit noch nicht completed oder PDF noch nicht readyAuf Audit-Ende warten; PDF-Status pollen
QUOTA_EXCEEDED (402)Plan erlaubt kein White-Label-Branding für die gewünschte brand_idbrand_id weglassen für ein schlichtes PDF oder Pro buchen
GONE (410)PDF-Datei fehlt auf der PlatteErneut mit POST /api/audits/:id/pdf einreihen

Häufig gestellte Fragen

Wie lange dauert das Rendern?

5 bis 20 Sekunden. Die Render-Pipeline startet ein Headless-Chromium, navigiert zum White-Label-HTML-Report und druckt zu PDF. JS-lastige Charts und große Findings-Listen drücken in den oberen Bereich.

Kann ich ein PDF einreihen, bevor der Audit fertig ist?

Nein. POST /api/audits/:id/pdf liefert 409 Conflict, wenn der Audit noch queued oder running ist. Warten, bis GET /api/audits/:id status: "completed" meldet, dann einreihen.

Wie bekomme ich ein gebrandetes PDF?

Einen Brand-Kit (Logo, Primärfarbe, Footer-Text) auf app.metricspot.com/settings/brands speichern und seine id in brand_id übergeben. Wird brand_id weggelassen, wendet die API automatisch deinen einzigen gespeicherten Brand-Kit an, falls vorhanden, sonst MetricSpot-Standard.

Wo wird das PDF gespeichert?

Im Filesystem des App-Servers, hinter der auth-gegateten Route GET /api/pdfs/:id/download. Die Download-URL ist Teil der JSON-Statusantwort und wird nur an das besitzende Konto ausgeliefert.

Quellen

Zuletzt aktualisiert 2026-05-14