api
POST /api/audits/:id/pdf y GET /api/pdfs/:id
Genera un PDF con tu marca a partir de una auditoría completada. Dos pasos: POST para encolar el render y GET para obtener el estado o el PDF cuando esté listo.
Qué hacen estos endpoints
La generación de PDF es un flujo en dos pasos. Primero, encola el render con POST /api/audits/:id/pdf. Después, haz polling de GET /api/pdfs/:id hasta que status === "ready" y descarga el fichero. El render lleva entre 5 y 20 segundos porque lanza un Chromium headless para imprimir el informe HTML white-label.
POST /api/audits/:id/pdfencola un job de render. El cuerpo es opcional y aceptalanguage(uno deen,es,de,fr,pt,it) ybrand_id(un brand kit guardado, onullpara branding MetricSpot estándar).GET /api/pdfs/:iddevuelve estado JSON por defecto. MandaAccept: application/pdfpara descargar el fichero cuandostatus === "ready".- Los PDFs con marca propia son solo Pro: la marca white-label (logo, color, footer) está gateada por plan. Los PDFs con marca MetricSpot existen en todos los planes de pago.
- La auditoría debe estar
status: "completed"para poder encolar el PDF. Pedir un PDF de una auditoría aún corriendo devuelve409.
Por qué importa
Los PDFs son el entregable para agencias, freelancers y consultoras. Un render nocturno automatizado significa que cada cliente abre su bandeja por la mañana con un informe fresco sin que nadie de la agencia haya pulsado un botón.
Workflows concretos:
- Un cron de agencia recorre cada URL de cliente guardada, llama a
POST /api/audits/:id/pdfcon elbrand_idde la agencia, hace polling hasta que esté listo y envía el PDF por email al cliente. - Un zap detecta nuevas auditorías en
GET /api/audits, dispara el render del PDF y sube el fichero a una carpeta de Dropbox compartida con el cliente.
Cómo usarlo
Paso 1: encola el render
POST /api/audits/12345/pdf HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{ "language": "es", "brand_id": null }
Paso 2: polling del estado
GET /api/pdfs/9876 HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx
Paso 3: descarga el fichero
GET /api/pdfs/9876 HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/pdf
curl (flujo completo)
TOKEN="ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"
# 1. Encolar
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":"es"}' | jq -r .pdf.id)
# 2. Polling del estado
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. Descarga
curl -o report.pdf https://app.metricspot.com/api/pdfs/$PDF_ID \
-H "authorization: Bearer $TOKEN" \
-H "accept: application/pdf"
Node (encolar, polling y descarga)
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: "es" }),
});
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": "es"}, 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)
Respuesta
POST /api/audits/:id/pdf
201 Created:
{
"pdf": {
"id": 9876,
"audit_id": 12345,
"brand_id": null,
"language": "es",
"status": "pending",
"created_at": "2026-05-14T10:20:01.000Z"
}
}
GET /api/pdfs/:id (estado 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"
}
}
Campos:
status:pending(en cola),rendering(Chromium corriendo),readyofailed.error_message: poblado cuandostatus === "failed".generated_at: timestamp ISO en el que el fichero terminó de renderizarse,nullhasta entonces.download_url: ruta relativa al endpoint de descarga, solo poblada cuandoready.
GET /api/pdfs/:id con Accept: application/pdf
Sirve el fichero con Content-Type: application/pdf y Content-Disposition: attachment; filename="<dominio>-audit.pdf". Status 200. Si el fichero falta en disco (raro, indica GC), devuelve 410 Gone.
Envoltorio de error
{ "error": "Audit not yet complete" }
Errores comunes
| Código | Cuándo | Acción |
|---|---|---|
UNAUTHORIZED (401) | Bearer ausente o inválido | Crea una clave en app.metricspot.com/settings/api-keys |
FORBIDDEN (403) | La auditoría o el PDF pertenecen a otra cuenta | Usa la clave de la cuenta propietaria |
AUDIT_NOT_FOUND (404) | El id de la auditoría no existe | Pasa una auditoría que poseas |
INVALID_URL (400) | :id no es un entero positivo | Pasa un id entero |
CONFLICT (409) | La auditoría aún no está completed, o el PDF aún no está ready | Espera a que termine; haz polling del PDF |
QUOTA_EXCEEDED (402) | El plan no permite branding white-label para el brand_id pedido | Quita brand_id para un PDF estándar, o sube a Pro |
GONE (410) | Fichero PDF ausente en disco | Vuelve a encolar con POST /api/audits/:id/pdf |
Preguntas frecuentes
¿Cuánto tarda el render?
Entre 5 y 20 segundos. La tubería arranca un Chromium headless, navega al informe HTML white-label e imprime a PDF. Gráficos cargados de JS y listas grandes de findings llevan al límite superior.
¿Puedo pedir el PDF antes de que termine la auditoría?
No. POST /api/audits/:id/pdf devuelve 409 Conflict si la auditoría sigue en queued o running. Espera a que GET /api/audits/:id reporte status: "completed" y entonces encolas.
¿Cómo obtengo un PDF con mi marca?
Guarda un brand kit (logo, color primario, texto de pie) en app.metricspot.com/settings/brands, y pasa su id en brand_id. Si omites brand_id, la API aplica automáticamente tu brand kit único si tienes uno guardado, cayendo a marca MetricSpot estándar en caso contrario.
¿Dónde se guarda el PDF?
En el sistema de ficheros del servidor de la app, detrás de la ruta autenticada GET /api/pdfs/:id/download. La URL de descarga forma parte del estado JSON y solo se sirve a la cuenta propietaria de la auditoría.
Fuentes
Última actualización 2026-05-14