api

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

Gere um PDF com a sua marca para uma auditoria completada. Fluxo em duas etapas: POST para enfileirar o render, depois GET o estado JSON ou o stream PDF.

O que estes endpoints fazem

A geração de PDF é um fluxo em duas etapas. Primeiro, enfileire o render com POST /api/audits/:id/pdf. Depois, faça polling em GET /api/pdfs/:id até status === "ready" e streame o ficheiro. O render demora 5 a 20 segundos porque inicia um Chromium headless para imprimir o relatório HTML white-label.

  • POST /api/audits/:id/pdf enfileira um job de render. O body é opcional e aceita language (um de en, es, de, fr, pt, it) e brand_id (um kit de marca guardado, ou null para marca MetricSpot simples).
  • GET /api/pdfs/:id devolve estado JSON por padrão. Envie Accept: application/pdf para streamar o ficheiro quando status === "ready".
  • PDFs brandeados são apenas Pro: branding white-label (logo, cor, rodapé) é gatado pelo plano. PDFs com marca MetricSpot simples estão disponíveis em todos os planos pagos.
  • A auditoria deve estar status: "completed" antes de um PDF poder ser enfileirado. PDF para uma auditoria ainda em execução devolve 409.

Por que importa

PDFs são o entregável de agências, freelancers e consultores. Um render automatizado todas as noites significa que cada cliente abre o email com um relatório fresco sem que ninguém na agência clique num botão.

Fluxos concretos:

  • Um cron de agência itera cada URL de cliente guardado, chama POST /api/audits/:id/pdf com o brand_id da agência, faz polling até pronto, e envia o PDF por email ao cliente.
  • Um zap Zapier observa novas auditorias em GET /api/audits, dispara um render PDF, e faz upload do ficheiro para uma pasta Dropbox partilhada com o cliente.

Como usar

Etapa 1: enfileirar o render

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

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

Etapa 2: polling do estado

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

Etapa 3: descarregar o ficheiro

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

curl (fluxo completo)

TOKEN="ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"

# 1. Enfileirar
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":"pt"}' | jq -r .pdf.id)

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

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

Node (enfileirar + polling + descarregar)

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: "pt" }),
});
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": "pt"}, 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)

Resposta

POST /api/audits/:id/pdf

201 Created:

{
  "pdf": {
    "id": 9876,
    "audit_id": 12345,
    "brand_id": null,
    "language": "pt",
    "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 (enfileirado), rendering (Chromium a correr), ready ou failed.
  • error_message: preenchido quando status === "failed".
  • generated_at: timestamp ISO de quando o ficheiro acabou de renderizar, null até pronto.
  • download_url: caminho relativo para o endpoint streamável de download, definido apenas quando ready.

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

Streama o ficheiro com Content-Type: application/pdf e Content-Disposition: attachment; filename="<domain>-audit.pdf". Código de estado é 200. Se o ficheiro estiver ausente do disco (raro, indica GC), devolve 410 Gone.

Envelope de erro

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

Erros comuns

CódigoQuandoAção
UNAUTHORIZED (401)Bearer ausente ou inválidoGere uma chave em app.metricspot.com/settings/api-keys
FORBIDDEN (403)Auditoria ou PDF pertence a outra contaUse a chave da conta proprietária
AUDIT_NOT_FOUND (404)Audit id não existePasse uma auditoria que possui
INVALID_URL (400):id não é um inteiro positivoPasse um id inteiro
CONFLICT (409)Auditoria ainda não completed, ou PDF ainda não readyAguarde a auditoria terminar; faça polling do estado do PDF
QUOTA_EXCEEDED (402)O plano não permite branding white-label para o brand_id pedidoOmita brand_id para um PDF simples, ou faça upgrade para Pro
GONE (410)Ficheiro PDF ausente do discoReenfileire com POST /api/audits/:id/pdf

Perguntas frequentes

Quanto tempo demora a renderização?

5 a 20 segundos. O pipeline de render inicia um Chromium headless, navega para o relatório HTML white-label, e imprime para PDF. Gráficos pesados em JS e listas grandes de findings empurram para o limite superior.

Posso enfileirar um PDF antes de a auditoria completar?

Não. POST /api/audits/:id/pdf devolve 409 Conflict se a auditoria ainda estiver queued ou running. Aguarde até GET /api/audits/:id reportar status: "completed", depois enfileire.

Como obtenho um PDF brandeado?

Guarde um kit de marca (logo, cor primária, texto de rodapé) em app.metricspot.com/settings/brands, depois passe o id em brand_id. Se omitir brand_id, a API aplica automaticamente o seu kit de marca único guardado quando existe, caindo para marca MetricSpot simples se não.

Onde é o PDF armazenado?

No sistema de ficheiros do servidor, atrás da rota auth-gated GET /api/pdfs/:id/download. O URL de download assinado faz parte da resposta JSON de estado e só é servido à conta proprietária da auditoria.

Fontes

Última atualização 2026-05-14