Docs menu

Reference

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.

FieldMeaning
rawThe machine value, always in canonical units
displayThe same value formatted for humans ("2.4 s")
ratingLighthouse 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.

ParameterSelects
auditIdThat exact audit. Wins over every other parameter
refThe page. Alone, resolves to its latest succeeded audit
gitHashWith ref, the audit for that commit
prNumberWith 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" }
	}
}
FieldRequiredDescription
refYesPage ref (the slug shown on your dashboard)
contextNoCI/CD metadata attached to the audit
context.git.commit.hashIf git provided7–64 character hex string
context.git.commit.messageIf git providedCommit message
context.git.commit.dateIf git providedISO 8601 date
auth.cookiesNoArray of cookie objects for authenticated pages
auth.headersNoKey-value headers for authenticated pages
auth.localStorageNoKey-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

StatuscodeMeaning
400invalid_request_bodyBody isn't valid JSON
400invalid_requestBody failed validation (see details)
400page_requires_authPage needs auth.cookies, auth.headers, or auth.localStorage
403read_only_tokenToken can't perform writes
404page_not_foundNo page with that ref in your account
429usage_limit_exceededCredit limit reached for the period
500audit_not_startedThe 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://…"
	}
}
FieldMeaning
insightsLighthouse insights with score (0–1, 1 = no issue) and estimated metricSavings in ms
warningsWarnings seen across the audit's runs (e.g. observation-window timeouts); null when clean
machineWhat the audit ran on — provider, region, CPU benchmark. Explains cross-region differences
windowedA run hit the observation window before the page settled, so the score is a partial-load measurement
scoreImputedA run's TBT audit errored and its performance score was imputed, so the reported score is a lower bound
reportUrlPresigned 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

StatuscodeMeaning
400invalid_audit_idThe id isn't a well-formed audit id
404audit_not_foundNo such audit in your organization
404audit_has_no_pageThe 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:

StatuscodeMeaning
400missing_selectorNeither auditId nor ref was given
400conflicting_selectorsBoth gitHash and prNumber were given
400invalid_pr_numberprNumber isn't a number
404no_audit_for_refThe page has no succeeded audit yet
404no_audit_for_git_hashNo audit for that commit on that page
404no_audit_for_pr_numberNo audit for that PR on that page
404page_not_foundNo 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:

StatuscodeMeaning
404baseline_not_foundbaseAuditId doesn't resolve
404no_baseline_for_git_hashNo audit for that baseline commit
404no_baseline_for_pr_numberNo audit for that baseline PR
404no_previous_auditHead is the page's first audit
409head_audit_not_runThe 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"
	}
}
FieldRequiredDescription
urlYesThe page to audit. Must be https
nameNoDisplay label. Defaults to the url
requiresAuthNoPage sits behind a login — audits must then supply auth
config.deviceNo"mobile" or "desktop". Defaults to your organization setting. Part of page identity
config.regionNoWhere the audit runs from. Defaults to your organization setting
config.warmupRunNoStabilized audits: warm-up pass + adaptive runs
scheduleNoOmit entirely for a one-off page with no schedule
schedule.frequencyIf schedule provided"interval" or "daily"
schedule.intervalHoursIf "interval"1–168
schedule.hourOfDayNo0–23, for a daily schedule
schedule.timeZoneNoIANA time zone for the schedule

Response

{
	"success": true,
	"data": {
		"ref": "homepage-x4f2",
		"id": "clx...",
		"url": "https://example.com",
		"created": true
	}
}

Errors

StatuscodeMeaning
400invalid_urlNot a valid https url
400invalid_requestBody failed validation (see details)
403read_only_tokenToken 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

StatuscodeMeaning
403read_only_tokenToken can't perform writes
404page_not_foundNo page with that ref in your account
405method_not_allowedWrong 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.

StatuscodeMeaning
400invalid_request_bodyBody isn't valid JSON
400invalid_requestBody failed validation — see details
400invalid_urlNot a valid https url
400invalid_audit_idMalformed audit id
400invalid_pr_numberprNumber isn't a number
400missing_selectorNeither auditId nor ref was given
400conflicting_selectorsgitHash and prNumber (or their base forms) both given
400page_requires_authPage needs auth.cookies, auth.headers, or auth.localStorage
401missing_authorizationNo Authorization: Bearer <token> header
401invalid_tokenToken isn't valid, or is missing org context
403invalid_token_typeWrong kind of token for this endpoint
403read_only_tokenEndpoint writes; the token is read-only
404page_not_foundNo page with that ref
404audit_not_foundNo such audit
404audit_has_no_pageThe audit's page has been deleted
404no_audit_for_refThe page has no succeeded audit yet
404no_audit_for_git_hashNo audit for that commit
404no_audit_for_pr_numberNo audit for that PR
404baseline_not_foundbaseAuditId doesn't resolve
404no_baseline_for_git_hashNo baseline audit for that commit
404no_baseline_for_pr_numberNo baseline audit for that PR
404no_previous_auditHead is the page's first audit
405method_not_allowedWrong HTTP method for the endpoint
409head_audit_not_runHead audit hasn't succeeded — nothing to compare
429usage_limit_exceededCredit limit reached for the period
500audit_not_startedThe audit couldn't be started
Help