api

REST API quickstart

MetricSpot's REST API in one page: Bearer auth at app.metricspot.com, six endpoints, rate limits, error codes, JSON over HTTPS.

What MetricSpot’s REST API does

MetricSpot’s REST API is a JSON-over-HTTPS interface at app.metricspot.com that wraps the same audit pipeline powering the dashboard. It exposes six endpoints: run an audit, fetch one by id, list recent audits, generate a branded PDF, and pull organic traffic from linked Google Analytics 4 and Search Console properties.

Capabilities:

  • Run a one-shot anonymous audit on any public URL (no auth, 1 per IP per 24 hours).
  • Queue a full audit asynchronously and poll until complete.
  • Fetch an audit envelope by id with score, module breakdown, and findings.
  • List recent audits, deduplicated by URL.
  • Trigger PDF generation and stream the file once ready.
  • Read a 28-day organic-traffic snapshot for any audit, cached 24 hours.

The API is the same surface MetricSpot’s own dashboard talks to. Anything you can see in the UI, an API client can reproduce.

Why it matters

A REST API turns audits into a building block. A CI bot can audit a preview deploy and fail the build on red findings. A Zapier flow can run a weekly audit and post the score to Slack. A reporting agency can pull PDFs for 200 clients on a schedule. None of these require browser automation: just HTTPS + a Bearer token.

Concrete workflows:

  • An audit-on-PR bot calls POST /api/audits when a preview URL deploys, polls GET /api/audits/:id until status === "completed", and posts the score delta back to the PR.
  • A weekly Zapier zap calls GET /api/audits to list every URL audited this month, then triggers a Slack message if any score dropped more than 5 points.
  • A white-label agency cron fetches POST /api/audits/:id/pdf overnight for every client domain and emails the rendered PDF the next morning.

How to use it

All endpoints except POST /api/public/audit require a Bearer API key minted from your MetricSpot account. The anonymous audit is cap-limited by IP and intentionally skips PageSpeed Insights to keep the cost predictable.

Auth header:

Authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Mint a key at app.metricspot.com/settings/api-keys. Up to 10 active keys per account. Keys start with ms_live_ and are shown once on creation.

Endpoints

MethodPathAuthRate limit
POST/api/public/auditnone1 per IP per 24 hours
POST/api/auditsBearerFree 10/mo, Starter 50/mo, Pro unlimited (plus per-domain cooldown)
GET/api/auditsBearernone beyond plan
GET/api/audits/:idBearernone beyond plan
POST/api/audits/:id/pdf then GET /api/pdfs/:idBearernone beyond plan
GET/api/audits/:id/googleBearer + Pro (GA4 / GSC linked)none beyond plan, cached 24h

The full machine-readable catalog lives at /.well-known/api-catalog per RFC 9727.

Quickstart: anonymous audit (no key)

curl -X POST https://app.metricspot.com/api/public/audit \
  -H "content-type: application/json" \
  -d '{"url": "https://example.com"}'

Quickstart: authenticated audit + poll

# 1. Queue
curl -X POST https://app.metricspot.com/api/audits \
  -H "authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{"url": "https://example.com"}'

# 2. Poll (replace 12345 with the returned audit_id)
curl https://app.metricspot.com/api/audits/12345 \
  -H "authorization: Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"

Quickstart: Node fetch

const res = await fetch("https://app.metricspot.com/api/audits", {
  method: "POST",
  headers: {
    "content-type": "application/json",
    authorization: "Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx",
  },
  body: JSON.stringify({ url: "https://example.com" }),
});
const { audit } = await res.json();
console.log(audit.id, audit.status);

Quickstart: Python httpx

import httpx

r = httpx.post(
    "https://app.metricspot.com/api/audits",
    headers={"authorization": "Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"},
    json={"url": "https://example.com"},
    timeout=30.0,
)
print(r.json()["audit"])

Response envelope

Every successful response is JSON. Mutating endpoints return 201 Created with the newly created resource; read endpoints return 200 OK with the resource and any related data (findings, plan, cooldown) the dashboard uses to render the same view.

Errors are JSON with at minimum error and an HTTP status code; some endpoints add message for human-readable detail.

Common errors

CodeWhenAction
UNAUTHORIZED (401)Missing or invalid Bearer tokenMint a key at app.metricspot.com/settings/api-keys
FORBIDDEN (403)Token valid but not allowed for this resource (e.g. another user’s audit)Use a key for the owning account
RATE_LIMITED (429)Anonymous IP cap or per-domain cooldownWait the indicated window
QUOTA_EXCEEDED (402)Monthly plan allowance used upUpgrade at app.metricspot.com/billing
INVALID_URL (400)URL not parseable, not absolute, or longer than 2000 charsPass an absolute https:// URL
AUDIT_NOT_FOUND (404)audit_id doesn’t exist or isn’t yoursList audits to find a valid id
UPSTREAM_FAILED (5xx)PageSpeed Insights, GA4, or GSC blipRetry with backoff

Frequently asked questions

Is the REST API free?

The anonymous endpoint (POST /api/public/audit) is free, capped at 1 audit per IP per 24 hours, and skips Core Web Vitals to keep cost predictable. The five authenticated endpoints count against your plan: Free 10 audits per month, Starter 50, Pro unlimited. List, get, PDF, and organic-traffic endpoints have no per-call cost beyond the audit they reference.

Where is the OpenAPI spec?

The machine-readable catalog is at https://metricspot.com/.well-known/api-catalog following RFC 9727. It links to the JSON spec for every endpoint and is what discovery agents read first.

How long does an audit take?

10 to 30 seconds for typical sites. JS-heavy targets or sites with large amounts of structured data can take up to 90 seconds. PageSpeed Insights is fetched in parallel with the crawler, so the bottleneck is usually Google’s PSI response time.

Can I use the API from the browser?

The anonymous endpoint accepts CORS from metricspot.com. Authenticated endpoints do not set permissive CORS by design: tokens belong on the server, never in client JavaScript. Use a serverless function or backend route as the proxy.

Sources

Last updated 2026-05-14