API Reference Paid
Full specification for the xcelera API. For a task-oriented guide — CI examples and the CLI — see API & CLI. The same data is available to LLM clients over the MCP server.
Base URL: https://xcelera.dev.
Authentication
All API requests require a Bearer token in the Authorization header.
Generate tokens in Settings → API Tokens. You can have up to 5 active tokens. Tokens cannot be retrieved after creation — store them securely.
Authorization: Bearer <token>
Tokens are either full-access or read-only. A read-only token can call
every GET endpoint; the write endpoints (starting an audit, registering and
archiving pages) reject it with read_only_token.
Response envelope
Every response — success or failure — uses the same envelope.
{ "success": true, "data": {} }{
"success": false,
"error": {
"code": "page_not_found",
"message": "Page not found for ref: homepage",
"hint": "Check the page ref on your dashboard.",
"details": "Machine-readable extra context, when there is any"
}
}Branch on code — it's a stable string. message and hint are prose for
humans and may be reworded; hint says what to do about the failure. details
is optional.
Metric shape
Every metric the API reports is a {raw, display, rating} object.
| Field | Meaning |
|---|---|
raw | The machine value, always in canonical units |
display | The same value formatted for humans ("2.4 s") |
rating | Lighthouse banding — "good", "needs-improvement", or "poor" |
Canonical units are milliseconds for LCP, FCP, SI and TBT, unitless for CLS,
and 0–100 for category scores. raw never needs parsing out of display.
A metric is absent rather than null when the audit produced no value for it.
Deltas (on /audits/compare and GET /pages) are in the same canonical units.
Selecting an audit
GET /api/v1/audits and GET /api/v1/audits/compare take the same selector
query parameters, so you can ask about a deploy without knowing internal ids.
| Parameter | Selects |
|---|---|
auditId | That exact audit. Wins over every other parameter |
ref | The page. Alone, resolves to its latest succeeded audit |
gitHash | With ref, the audit for that commit |
prNumber | With ref, the audit for that pull request |
gitHash and prNumber are mutually exclusive — passing both is
conflicting_selectors. Passing neither auditId nor ref is
missing_selector.
Only auditId resolves an audit that hasn't succeeded. A ref-based selector
looks at succeeded audits, so it won't return an audit that is still running —
poll a freshly started audit by its auditId and watch status.
POST /api/v1/audits
Start a Lighthouse audit for a page. Requires a full-access token. Returns as
soon as the audit is scheduled — poll GET /api/v1/audits/:auditId for the
result.
Request body
{
"ref": "homepage",
"context": {
"service": "GitHub Actions",
"buildNumber": "42",
"buildUrl": "https://github.com/org/repo/actions/runs/123",
"prNumber": 7,
"git": {
"owner": "org",
"repo": "repo",
"branch": "feature/faster-lcp",
"commit": {
"hash": "abc1234",
"message": "Optimize hero image loading",
"date": "2025-01-15T10:30:00Z",
"author": "developer@example.com"
}
}
},
"auth": {
"cookies": [
{ "name": "session", "value": "abc123", "domain": "example.com" }
],
"headers": { "X-Custom-Auth": "token123" },
"localStorage": { "auth_token": "token123" }
}
}| Field | Required | Description |
|---|---|---|
ref | Yes | Page ref (the slug shown on your dashboard) |
context | No | CI/CD metadata attached to the audit |
context.git.commit.hash | If git provided | 7–64 character hex string |
context.git.commit.message | If git provided | Commit message |
context.git.commit.date | If git provided | ISO 8601 date |
auth.cookies | No | Array of cookie objects for authenticated pages |
auth.headers | No | Key-value headers for authenticated pages |
auth.localStorage | No | Key-value localStorage entries, set on the page's origin before the audit runs — see authenticated pages |
Device and region come from the page itself — set them when you register the page, not per audit.
Response
{
"success": true,
"data": {
"auditId": "ah7n75i5uxk6fce9wanzeq8d",
"url": "https://example.com",
"status": "scheduled",
"page": { "id": "clx...", "name": "Homepage", "ref": "homepage" }
}
}integrations.github is also present when the audit is linked to a GitHub
check run.
Errors
| Status | code | Meaning |
|---|---|---|
400 | invalid_request_body | Body isn't valid JSON |
400 | invalid_request | Body failed validation (see details) |
400 | page_requires_auth | Page needs auth.cookies, auth.headers, or auth.localStorage |
403 | read_only_token | Token can't perform writes |
404 | page_not_found | No page with that ref in your account |
429 | usage_limit_exceeded | Credit limit reached for the period |
500 | audit_not_started | The audit couldn't be started |
GET /api/v1/audits/:auditId
One audit in full, by id. This is the endpoint to poll after starting an audit —
it resolves an audit in any state, and status tells you where it is
(Scheduled, Running, Succeeded, Failed).
Response
{
"success": true,
"data": {
"auditId": "ah7n75i5uxk6fce9wanzeq8d",
"ref": "homepage",
"name": "Homepage",
"url": "https://example.com",
"status": "Succeeded",
"runAt": "2026-07-15T10:32:04.000Z",
"source": "Api",
"git": {
"hash": "abc1234",
"branch": "main",
"prNumber": 7,
"commitMessage": "Optimize hero image loading",
"author": "developer@example.com"
},
"metrics": {
"categories": {
"performance": {
"raw": 78,
"display": 78,
"rating": "needs-improvement"
},
"accessibility": { "raw": 96, "display": 96, "rating": "good" },
"bestPractices": { "raw": 100, "display": 100, "rating": "good" },
"seo": { "raw": 92, "display": 92, "rating": "good" }
},
"audits": {
"lcp": {
"raw": 2400,
"display": "2.4 s",
"rating": "needs-improvement"
},
"tbt": { "raw": 180, "display": "180 ms", "rating": "good" },
"cls": { "raw": 0.02, "display": "0.02", "rating": "good" },
"fcp": { "raw": 1100, "display": "1.1 s", "rating": "good" },
"si": { "raw": 3200, "display": "3.2 s", "rating": "needs-improvement" }
}
},
"insights": [
{
"id": "lcp-discovery",
"title": "LCP request discovery",
"score": 0.5,
"displayValue": "Est savings of 620 ms",
"metricSavings": { "LCP": 620 }
}
],
"warnings": null,
"machine": {
"provider": "gce",
"region": "us-central1",
"cpuSlowdownMultiplier": 1.8,
"benchmarkIndex": 1450,
"durationMs": 41200,
"warmupRun": false
},
"windowed": false,
"scoreImputed": false,
"reportUrl": "https://…"
}
}| Field | Meaning |
|---|---|
insights | Lighthouse insights with score (0–1, 1 = no issue) and estimated metricSavings in ms |
warnings | Warnings seen across the audit's runs (e.g. observation-window timeouts); null when clean |
machine | What the audit ran on — provider, region, CPU benchmark. Explains cross-region differences |
windowed | A run hit the observation window before the page settled, so the score is a partial-load measurement |
scoreImputed | A run's TBT audit errored and its performance score was imputed, so the reported score is a lower bound |
reportUrl | Presigned link to the full HTML Lighthouse report. Valid for one hour — fetch it, don't store it |
Treat windowed or scoreImputed as a caveat on the score, not a failure. A
regression that only shows up on a windowed audit is usually measurement, not
your deploy.
Errors
| Status | code | Meaning |
|---|---|---|
400 | invalid_audit_id | The id isn't a well-formed audit id |
404 | audit_not_found | No such audit in your organization |
404 | audit_has_no_page | The audit's page has been deleted |
GET /api/v1/audits
The same audit payload as above, resolved by selector instead of by id. This is a lookup, not a list — it returns exactly one audit.
curl -H "Authorization: Bearer $XCELERA_TOKEN" \
"https://xcelera.dev/api/v1/audits?ref=homepage"
curl -H "Authorization: Bearer $XCELERA_TOKEN" \
"https://xcelera.dev/api/v1/audits?ref=homepage&gitHash=abc1234"Errors
Everything GET /audits/:auditId returns, plus the selector arms:
| Status | code | Meaning |
|---|---|---|
400 | missing_selector | Neither auditId nor ref was given |
400 | conflicting_selectors | Both gitHash and prNumber were given |
400 | invalid_pr_number | prNumber isn't a number |
404 | no_audit_for_ref | The page has no succeeded audit yet |
404 | no_audit_for_git_hash | No audit for that commit on that page |
404 | no_audit_for_pr_number | No audit for that PR on that page |
404 | page_not_found | No page with that ref |
GET /api/v1/audits/compare
Two audits of the same page, diffed: metric deltas and insight movement. Use it to answer "did this commit make anything worse".
The head audit uses the selector parameters. The baseline
uses baseAuditId, baseGitHash or basePrNumber, and defaults to the audit
that ran immediately before head. baseGitHash / basePrNumber resolve
against the head audit's page.
Comparison is always within one page definition, so both sides are the same URL, device and region by construction.
curl -H "Authorization: Bearer $XCELERA_TOKEN" \
"https://xcelera.dev/api/v1/audits/compare?ref=homepage&prNumber=7"Response
{
"success": true,
"data": {
"head": {
"auditId": "ah7n75i5uxk6fce9wanzeq8d",
"runAt": "2026-07-15T10:32:04.000Z",
"source": "Api",
"git": {
"hash": "abc1234",
"branch": "feature/faster-lcp",
"prNumber": 7
}
},
"base": {
"auditId": "kd91h3f0aa72bcx1qq0mzz44",
"runAt": "2026-07-14T22:05:11.000Z",
"source": "Scheduler",
"git": { "hash": "9f8e7d6", "branch": "main" }
},
"deltas": {
"performance": { "change": -6, "direction": "worse", "previous": 84 },
"lcp": { "change": 410, "direction": "worse", "previous": 1990 },
"tbt": { "change": -20, "direction": "better", "previous": 200 },
"cls": { "change": 0, "direction": "unchanged", "previous": 0.02 }
},
"insightDiff": {
"new": [
{
"id": "render-blocking-insight",
"title": "Render blocking requests",
"score": 0.4
}
],
"resolved": [],
"worsened": [
{
"id": "lcp-discovery",
"title": "LCP request discovery",
"before": 0.9,
"after": 0.5
}
],
"improved": []
}
}
}change is head minus base in canonical units; direction is
"better" / "worse" / "unchanged" and already accounts for metrics where
lower is better. Insight scores are 0–1, so a lower after is worse.
Judging whether a delta is real is qualitative: a few points of performance score is normal run-to-run variation. Compare the delta against how much that page's scores usually move — see Audit stability.
Errors
Everything GET /audits returns, plus the baseline arms:
| Status | code | Meaning |
|---|---|---|
404 | baseline_not_found | baseAuditId doesn't resolve |
404 | no_baseline_for_git_hash | No audit for that baseline commit |
404 | no_baseline_for_pr_number | No audit for that baseline PR |
404 | no_previous_audit | Head is the page's first audit |
409 | head_audit_not_run | The head audit hasn't succeeded, so it has no metrics to compare |
GET /api/v1/pages
Every page your organization tracks, with the scores of its latest audit. This is how you find refs.
{
"success": true,
"data": {
"pages": [
{
"ref": "homepage",
"name": "Homepage",
"url": "https://example.com",
"auditStatus": "Succeeded",
"lastAuditAt": "2026-07-15T10:32:04.000Z",
"auditId": "ah7n75i5uxk6fce9wanzeq8d",
"scores": {
"performance": {
"raw": 78,
"display": 78,
"rating": "needs-improvement"
},
"lcp": {
"raw": 2400,
"display": "2.4 s",
"rating": "needs-improvement"
},
"tbt": { "raw": 180, "display": "180 ms", "rating": "good" },
"cls": { "raw": 0.02, "display": "0.02", "rating": "good" }
},
"deltas": {
"performance": { "change": -6, "direction": "worse" },
"previousAuditRunAt": "2026-07-14T22:05:11.000Z"
},
"history7d": [
{
"date": "2026-07-15",
"performance": 78,
"lcp": 2400,
"tbt": 180,
"cls": 0.02
}
],
"warning": "information",
"warningMessages": ["A run hit the observation window."]
}
]
}
}history7d has one point per day — the latest audit that day. deltas compare
the latest audit against the one before it. warning is "information" or
"critical", and is absent when the page is healthy.
POST /api/v1/pages
Register a page to audit. Requires a full-access token. Returns the page's
ref, which every audit endpoint takes.
Registration is an upsert: the same url and device is one page, so a deploy
script can call this on every run without accumulating duplicates. An existing
page keeps its own name, config and schedule, and comes back with
created: false.
{
"url": "https://example.com",
"name": "Homepage",
"requiresAuth": false,
"config": { "device": "mobile", "region": "us-central1", "warmupRun": false },
"schedule": {
"frequency": "daily",
"hourOfDay": 3,
"timeZone": "Pacific/Auckland"
}
}| Field | Required | Description |
|---|---|---|
url | Yes | The page to audit. Must be https |
name | No | Display label. Defaults to the url |
requiresAuth | No | Page sits behind a login — audits must then supply auth |
config.device | No | "mobile" or "desktop". Defaults to your organization setting. Part of page identity |
config.region | No | Where the audit runs from. Defaults to your organization setting |
config.warmupRun | No | Stabilized audits: warm-up pass + adaptive runs |
schedule | No | Omit entirely for a one-off page with no schedule |
schedule.frequency | If schedule provided | "interval" or "daily" |
schedule.intervalHours | If "interval" | 1–168 |
schedule.hourOfDay | No | 0–23, for a daily schedule |
schedule.timeZone | No | IANA time zone for the schedule |
Response
{
"success": true,
"data": {
"ref": "homepage-x4f2",
"id": "clx...",
"url": "https://example.com",
"created": true
}
}Errors
| Status | code | Meaning |
|---|---|---|
400 | invalid_url | Not a valid https url |
400 | invalid_request | Body failed validation (see details) |
403 | read_only_token | Token can't perform writes |
DELETE /api/v1/pages/:ref
Archive a page. Requires a full-access token. The page stops being audited and
disappears from GET /api/v1/pages; its audits are kept but no longer surfaced.
{ "success": true, "data": { "ref": "homepage-x4f2", "archived": true } }Archiving is not reversible through the API. Registering the same url again creates a new page with a new ref and no history — it does not bring the archived page back.
Errors
| Status | code | Meaning |
|---|---|---|
403 | read_only_token | Token can't perform writes |
404 | page_not_found | No page with that ref in your account |
405 | method_not_allowed | Wrong HTTP method for the endpoint |
Rate limits
/api/v1 requests are limited per token, and per IP on top of that. Over the
limit you get a 429 with a Retry-After header — back off for that many
seconds rather than retrying immediately.
Error codes
Every code the API can return.
| Status | code | Meaning |
|---|---|---|
400 | invalid_request_body | Body isn't valid JSON |
400 | invalid_request | Body failed validation — see details |
400 | invalid_url | Not a valid https url |
400 | invalid_audit_id | Malformed audit id |
400 | invalid_pr_number | prNumber isn't a number |
400 | missing_selector | Neither auditId nor ref was given |
400 | conflicting_selectors | gitHash and prNumber (or their base forms) both given |
400 | page_requires_auth | Page needs auth.cookies, auth.headers, or auth.localStorage |
401 | missing_authorization | No Authorization: Bearer <token> header |
401 | invalid_token | Token isn't valid, or is missing org context |
403 | invalid_token_type | Wrong kind of token for this endpoint |
403 | read_only_token | Endpoint writes; the token is read-only |
404 | page_not_found | No page with that ref |
404 | audit_not_found | No such audit |
404 | audit_has_no_page | The audit's page has been deleted |
404 | no_audit_for_ref | The page has no succeeded audit yet |
404 | no_audit_for_git_hash | No audit for that commit |
404 | no_audit_for_pr_number | No audit for that PR |
404 | baseline_not_found | baseAuditId doesn't resolve |
404 | no_baseline_for_git_hash | No baseline audit for that commit |
404 | no_baseline_for_pr_number | No baseline audit for that PR |
404 | no_previous_audit | Head is the page's first audit |
405 | method_not_allowed | Wrong HTTP method for the endpoint |
409 | head_audit_not_run | Head audit hasn't succeeded — nothing to compare |
429 | usage_limit_exceeded | Credit limit reached for the period |
500 | audit_not_started | The audit couldn't be started |