api
POST /api/audits/:id/pdf and GET /api/pdfs/:id
Generate a branded PDF report for a completed audit. Two-step: POST to queue the render, then GET the JSON status or the PDF stream when ready.
What these endpoints do
PDF generation is a two-step flow. First, queue the render with POST /api/audits/:id/pdf. Then poll GET /api/pdfs/:id until status === "ready" and stream the file. The render takes 5 to 20 seconds because it spawns a headless Chromium to print the white-label HTML report.
POST /api/audits/:id/pdfqueues a render job. Body is optional and acceptslanguage(one ofen,es,de,fr,pt,it) andbrand_id(a saved brand kit, ornullfor plain MetricSpot branding).GET /api/pdfs/:idreturns JSON status by default. SendAccept: application/pdfto stream the file oncestatus === "ready".- Branded PDFs are Pro-only: white-label branding (logo, color, footer) is gated by the plan. Plain MetricSpot-branded PDFs are available on every paid plan.
- The audit must be
status: "completed"before a PDF can be queued. PDF for a still-running audit returns409.
Why it matters
PDFs are the deliverable for agencies, freelancers, and consultants. An automated nightly render means every client opens their inbox to a fresh report without anyone in the agency clicking a button.
Concrete workflows:
- An agency cron iterates every saved client URL, calls
POST /api/audits/:id/pdfwith the agency’sbrand_id, polls until ready, and emails the PDF to the client. - A Zapier zap watches for new audits in
GET /api/audits, triggers a PDF render, and uploads the file to a Dropbox folder shared with the client.
How to use it
Step 1: queue the render
POST /api/audits/12345/pdf HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{ "language": "en", "brand_id": null }
Step 2: poll for status
GET /api/pdfs/9876 HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx
Step 3: download the file
GET /api/pdfs/9876 HTTP/1.1
Host: app.metricspot.com
Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx
Accept: application/pdf
curl (full flow)
TOKEN="ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"
# 1. Queue
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":"en"}' | jq -r .pdf.id)
# 2. Poll status
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. Download
curl -o report.pdf https://app.metricspot.com/api/pdfs/$PDF_ID \
-H "authorization: Bearer $TOKEN" \
-H "accept: application/pdf"
Node (queue + poll + 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: "en" }),
});
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": "en"}, 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)
Response
POST /api/audits/:id/pdf
201 Created:
{
"pdf": {
"id": 9876,
"audit_id": 12345,
"brand_id": null,
"language": "en",
"status": "pending",
"created_at": "2026-05-14T10:20:01.000Z"
}
}
GET /api/pdfs/:id (JSON status)
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"
}
}
Fields:
status:pending(queued),rendering(Chromium running),ready, orfailed.error_message: populated whenstatus === "failed".generated_at: ISO timestamp the file finished rendering,nulluntil ready.download_url: relative path to the streamable download endpoint, only set whenready.
GET /api/pdfs/:id with Accept: application/pdf
Streams the file with Content-Type: application/pdf and Content-Disposition: attachment; filename="<domain>-audit.pdf". Status code is 200. If the file is missing from disk (rare, indicates GC), returns 410 Gone.
Error envelope
{ "error": "Audit not yet complete" }
Common errors
| Code | When | Action |
|---|---|---|
UNAUTHORIZED (401) | Missing or invalid Bearer token | Mint a key at app.metricspot.com/settings/api-keys |
FORBIDDEN (403) | Audit or PDF belongs to a different account | Use the owning account’s key |
AUDIT_NOT_FOUND (404) | Audit id doesn’t exist | Pass an audit you own |
INVALID_URL (400) | :id not a positive integer | Pass an integer id |
CONFLICT (409) | Audit not yet completed, or PDF not yet ready | Wait for the audit to finish; poll the PDF status |
QUOTA_EXCEEDED (402) | Plan doesn’t allow white-label branding for the requested brand_id | Drop brand_id for a plain PDF, or upgrade to Pro |
GONE (410) | PDF file missing on disk | Re-queue with POST /api/audits/:id/pdf |
Frequently asked questions
How long does rendering take?
5 to 20 seconds. The render pipeline spawns a headless Chromium, navigates to the white-label HTML report, and prints to PDF. JS-heavy charts and large finding lists push toward the upper end.
Can I queue a PDF before the audit completes?
No. POST /api/audits/:id/pdf returns 409 Conflict if the audit is still queued or running. Wait until GET /api/audits/:id reports status: "completed", then queue.
How do I get a branded PDF?
Save a brand kit (logo, primary color, footer text) at app.metricspot.com/settings/brands, then pass its id in brand_id. If you omit brand_id, the API auto-applies your singular saved brand kit when present, falling back to plain MetricSpot branding.
Where is the PDF stored?
On the app server’s filesystem, behind the auth-gated GET /api/pdfs/:id/download route. The signed download URL is part of the JSON status response and is only served to the account that owns the audit.
Sources
Last updated 2026-05-14