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 realaudit_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 tocompleted.
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/auditswhen a preview deploy ships, captures theaudit_id, pollsGET /api/audits/:idevery 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:nulluntil 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
| Code | When | Action |
|---|---|---|
UNAUTHORIZED (401) | Missing or invalid Bearer token | Mint a key at app.metricspot.com/settings/api-keys |
QUOTA_EXCEEDED (402) | Monthly plan allowance used | Upgrade at app.metricspot.com/billing |
INVALID_URL (400) | URL not parseable or > 2000 chars | Pass an absolute https:// URL |
RATE_LIMITED (429) | Per-domain or global cooldown | Wait the indicated window before retrying the same URL |
UPSTREAM_FAILED (5xx) | PSI or crawler upstream blip | Retry 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