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/pdfmette in coda un job di render. Il body è opzionale e accettalanguage(uno dien,es,de,fr,pt,it) ebrand_id(un brand kit salvato, onullper branding MetricSpot semplice).GET /api/pdfs/:idrestituisce stato JSON per default. InviaAccept: application/pdfper fare streaming del file quandostatus === "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 restituisce409.
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/pdfcon ilbrand_iddell’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),readyofailed.error_message: popolato quandostatus === "failed".generated_at: timestamp ISO di quando il file ha finito di renderizzare,nullfinché non pronto.download_url: percorso relativo all’endpoint di download streamabile, definito solo quandoready.
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
| Codice | Quando | Azione |
|---|---|---|
UNAUTHORIZED (401) | Bearer mancante o non valido | Genera una chiave su app.metricspot.com/settings/api-keys |
FORBIDDEN (403) | Audit o PDF appartiene a un altro account | Usa la chiave dell’account proprietario |
AUDIT_NOT_FOUND (404) | Audit id non esiste | Passa un audit che possiedi |
INVALID_URL (400) | :id non è un intero positivo | Passa un id intero |
CONFLICT (409) | Audit non ancora completed, o PDF non ancora ready | Aspetta che l’audit finisca; polla lo stato del PDF |
QUOTA_EXCEEDED (402) | Il piano non permette branding white-label per il brand_id richiesto | Ometti brand_id per un PDF semplice, o passa a Pro |
GONE (410) | File PDF mancante dal disco | Re-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