api

POST /api/audits

Queue a full SEO audit with Core Web Vitals. Returns audit_id + status: queued in 1-2s. Counts against plan, respects per-domain cooldowns.

What this endpoint does

POST /api/audits queues a full SEO and AI-readability audit and returns immediately with the audit envelope. It is the authenticated, full-fat counterpart to POST /api/public/audit.

  • Queues asynchronously and returns in 1-2 seconds with status: "queued" and a real audit_id.
  • Pulls Core Web Vitals (LCP, CLS, INP) from Google PageSpeed Insights as part of the run.
  • Counts against the user’s plan allowance (Free 10/mo, Starter 50/mo, Pro unlimited).
  • Respects per-domain cooldowns: 24 hours between audits of the same exact URL, plus a short global cooldown between any two audits.
  • Returns the same shape as GET /api/audits/:id. Findings are empty until the audit transitions to completed.

Why it matters

POST /api/audits is the right endpoint whenever you need a persistent audit you can fetch later, a PDF, organic-traffic data, or Core Web Vitals. Anonymous audits cover roughly 90 checks but skip PSI; this endpoint covers everything.

Concrete workflows:

  • An audit-on-PR bot calls POST /api/audits when a preview deploy ships, captures the audit_id, polls GET /api/audits/:id every 5 seconds for up to 60 seconds, and posts a delta comment versus the last audit on the same URL.
  • A weekly cron agent iterates the user’s most-visited landing pages and queues a fresh audit for each, then mails a digest from GET /api/audits.

How to use it

The endpoint is async by design. Treat the immediate 201 Created response as an acknowledgement, then poll GET /api/audits/:id with the returned audit_id every few seconds. Typical end-to-end completion is 10 to 30 seconds; allow up to 90 seconds for slow targets.

Request

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

{ "url": "https://example.com" }

The url field is required, must be absolute (https://...), parseable, and at most 2000 characters.

curl

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"}'

Node (queue + poll)

const headers = {
  authorization: "Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx",
  "content-type": "application/json",
};

const queueRes = await fetch("https://app.metricspot.com/api/audits", {
  method: "POST",
  headers,
  body: JSON.stringify({ url: "https://example.com" }),
});
const { audit } = await queueRes.json();
const id = audit.id;

while (true) {
  await new Promise((r) => setTimeout(r, 3000));
  const r = await fetch(`https://app.metricspot.com/api/audits/${id}`, { headers });
  const data = await r.json();
  if (["completed", "failed"].includes(data.audit.status)) {
    console.log(data.audit.status, data.audit.score);
    break;
  }
}

Python httpx

import httpx, time

HEADERS = {"authorization": "Bearer ms_live_xxxxxxxxxxxxxxxxxxxxxxxx"}

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

for _ in range(30):
    time.sleep(3)
    r = httpx.get(f"https://app.metricspot.com/api/audits/{audit_id}", headers=HEADERS, timeout=30.0)
    audit = r.json()["audit"]
    if audit["status"] in ("completed", "failed"):
        print(audit["status"], audit["score"])
        break

Response

201 Created:

{
  "audit": {
    "id": 12345,
    "domain": "example.com",
    "url": "https://example.com",
    "status": "queued",
    "score": null,
    "created_at": "2026-05-14T10:18:04.000Z"
  }
}

Fields on the queued response:

  • id: integer audit id, used in every follow-up call (GET /api/audits/:id, PDF, Google).
  • domain: hostname extracted from the URL.
  • url: echoed back from the request.
  • status: always "queued" on initial response. Transitions to "running", then "completed" or "failed".
  • score: null until the audit completes.
  • created_at: ISO 8601 timestamp.

Once status === "completed", GET /api/audits/:id returns the full envelope with score, module_scores, and the findings array (see api-get-audit).

Error envelope

{ "error": "Quota exceeded", "message": "Upgrade to Starter for 50 audits per month." }

Common errors

CodeWhenAction
UNAUTHORIZED (401)Missing or invalid Bearer tokenMint a key at app.metricspot.com/settings/api-keys
QUOTA_EXCEEDED (402)Monthly plan allowance usedUpgrade at app.metricspot.com/billing
INVALID_URL (400)URL not parseable or > 2000 charsPass an absolute https:// URL
RATE_LIMITED (429)Per-domain or global cooldownWait the indicated window before retrying the same URL
UPSTREAM_FAILED (5xx)PSI or crawler upstream blipRetry once with backoff

Frequently asked questions

How long until the audit completes?

10 to 30 seconds for typical sites. JS-heavy targets or pages with large amounts of structured data can take up to 90 seconds. Poll GET /api/audits/:id every 3 to 5 seconds: the audit is fully computed once status flips to completed.

Does a failed audit cost an audit?

A failed audit does not consume plan allowance. Quota only decrements when the run completes successfully and findings are stored. PSI rate-limit failures are retried internally before being surfaced.

Why is there a per-domain cooldown?

The same URL audited twice in five minutes returns the same findings: it wastes PSI quota and confuses traffic dashboards. The 24-hour cooldown per URL is enforced server-side. The error message includes the exact retry_at timestamp so clients can schedule a re-queue.

Can I queue many audits in parallel?

Yes, up to the global cooldown (a few seconds between any two audits per account). For batch runs of 50+ URLs, space them out client-side or use Pro, which lifts both monthly quota and tightens the global throttle.

Sources

Last updated 2026-05-14