api

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

Genera un PDF brandizzato per un audit completato. Flusso a due passi: POST per mettere in coda il render, poi GET lo stato JSON o lo stream PDF.

Cosa fanno questi endpoint

La generazione PDF è un flusso a due passi. Prima, metti in coda il render con POST /api/audits/:id/pdf. Poi polla GET /api/pdfs/:id finché status === "ready" e fai streaming del file. Il render richiede 5 a 20 secondi perché avvia un Chromium headless per stampare il report HTML white-label.

  • POST /api/audits/:id/pdf mette in coda un job di render. Il body è opzionale e accetta language (uno di en, es, de, fr, pt, it) e brand_id (un brand kit salvato, o null per branding MetricSpot semplice).
  • GET /api/pdfs/:id restituisce stato JSON per default. Invia Accept: application/pdf per fare streaming del file quando status === "ready".
  • I PDF brandizzati sono solo Pro: il branding white-label (logo, colore, footer) è gatato dal piano. PDF brandizzati MetricSpot semplici sono disponibili su ogni piano a pagamento.
  • L’audit deve essere status: "completed" prima che un PDF possa essere messo in coda. PDF per un audit ancora in esecuzione restituisce 409.

Perché è importante

I PDF sono il deliverable di agenzie, freelancer e consulenti. Un render automatizzato notturno significa che ogni cliente apre l’inbox con un report fresco senza che nessuno in agenzia clicchi un bottone.

Flussi concreti:

  • Un cron di agenzia itera ogni URL cliente salvato, chiama POST /api/audits/:id/pdf con il brand_id dell’agenzia, polla finché pronto, e invia il PDF per email al cliente.
  • Uno zap Zapier osserva nuovi audit in GET /api/audits, fa scattare un render PDF, e carica il file in una cartella Dropbox condivisa con il cliente.

Come usarlo

Passo 1: mettere in coda il render

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

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

Passo 2: pollare lo stato

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

Passo 3: scaricare il file

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

curl (flusso completo)

TOKEN="ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"

# 1. Mettere in coda
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":"it"}' | jq -r .pdf.id)

# 2. Pollare
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 fallito"; exit 1; }
  sleep 2
done

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

Node (in coda + polling + 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: "it" }),
});
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": "it"}, 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)

Risposta

POST /api/audits/:id/pdf

201 Created:

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

GET /api/pdfs/:id (stato JSON)

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"
  }
}

Campi:

  • status: pending (in coda), rendering (Chromium in esecuzione), ready o failed.
  • error_message: popolato quando status === "failed".
  • generated_at: timestamp ISO di quando il file ha finito di renderizzare, null finché non pronto.
  • download_url: percorso relativo all’endpoint di download streamabile, definito solo quando ready.

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

Fa streaming del file con Content-Type: application/pdf e Content-Disposition: attachment; filename="<domain>-audit.pdf". Codice di stato è 200. Se il file manca dal disco (raro, indica GC), restituisce 410 Gone.

Busta di errore

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

Errori comuni

CodiceQuandoAzione
UNAUTHORIZED (401)Bearer mancante o non validoGenera una chiave su app.metricspot.com/settings/api-keys
FORBIDDEN (403)Audit o PDF appartiene a un altro accountUsa la chiave dell’account proprietario
AUDIT_NOT_FOUND (404)Audit id non esistePassa un audit che possiedi
INVALID_URL (400):id non è un intero positivoPassa un id intero
CONFLICT (409)Audit non ancora completed, o PDF non ancora readyAspetta che l’audit finisca; polla lo stato del PDF
QUOTA_EXCEEDED (402)Il piano non permette branding white-label per il brand_id richiestoOmetti brand_id per un PDF semplice, o passa a Pro
GONE (410)File PDF mancante dal discoRe-metti in coda con POST /api/audits/:id/pdf

Domande frequenti

Quanto tempo richiede il rendering?

5 a 20 secondi. La pipeline di render avvia un Chromium headless, naviga al report HTML white-label, e stampa in PDF. Grafici ricchi di JS e liste grandi di findings spingono verso il limite superiore.

Posso mettere in coda un PDF prima che l’audit completi?

No. POST /api/audits/:id/pdf restituisce 409 Conflict se l’audit è ancora queued o running. Aspetta finché GET /api/audits/:id riporta status: "completed", poi metti in coda.

Come ottengo un PDF brandizzato?

Salva un brand kit (logo, colore primario, testo footer) su app.metricspot.com/settings/brands, poi passa il suo id in brand_id. Se ometti brand_id, l’API applica automaticamente il tuo brand kit singolo salvato quando presente, fallendo su branding MetricSpot semplice.

Dov’è memorizzato il PDF?

Sul filesystem del server app, dietro la route auth-gated GET /api/pdfs/:id/download. L’URL di download firmato fa parte della risposta JSON di stato ed è servito solo all’account proprietario dell’audit.

Fonti

Ultimo aggiornamento 2026-05-14