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/pdf encola un job de render. El cuerpo es opcional y acepta language (uno de en, es, de, fr, pt, it) y brand_id (un brand kit guardado, o null para branding MetricSpot estándar).
  • GET /api/pdfs/:id devuelve estado JSON por defecto. Manda Accept: application/pdf para descargar el fichero cuando status === "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 devuelve 409.

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/pdf con el brand_id de 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), ready o failed.
  • error_message: poblado cuando status === "failed".
  • generated_at: timestamp ISO en el que el fichero terminó de renderizarse, null hasta entonces.
  • download_url: ruta relativa al endpoint de descarga, solo poblada cuando ready.

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ódigoCuándoAcción
UNAUTHORIZED (401)Bearer ausente o inválidoCrea una clave en app.metricspot.com/settings/api-keys
FORBIDDEN (403)La auditoría o el PDF pertenecen a otra cuentaUsa la clave de la cuenta propietaria
AUDIT_NOT_FOUND (404)El id de la auditoría no existePasa una auditoría que poseas
INVALID_URL (400):id no es un entero positivoPasa un id entero
CONFLICT (409)La auditoría aún no está completed, o el PDF aún no está readyEspera a que termine; haz polling del PDF
QUOTA_EXCEEDED (402)El plan no permite branding white-label para el brand_id pedidoQuita brand_id para un PDF estándar, o sube a Pro
GONE (410)Fichero PDF ausente en discoVuelve 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