Python · Code recipe
Run a Lighthouse audit in Python
Score any page for performance, accessibility, best practices and SEO, in Python 3.9+ with requests. Every program on this page runs as it stands — each one was run against a stub of the API before it was published.
By Roger Campos · Last updated: September 2026
TL;DR
To run a Lighthouse audit in Python, POST a URL and a device to https://urlpipe.dev/lighthouse and read the JSON with res.json(): four category scores from 0 to 1 and the lab metrics, LCP, CLS and TBT among them. An audit takes around 15 seconds, so the async variant with a webhook suits production. It costs 2 credits.
Free plan, no credit card. 1,000 credits a month.
One POST to /lighthouse runs a real Lighthouse audit in Chrome and answers with the four category scores — performance, accessibility, best practices, SEO — and the lab metrics behind the performance score. device picks mobile (the default, throttled CPU and network) or desktop.
In Python the report is read with res.json(). Scores run from 0 to 1, so the program multiplies by 100 to print what the Lighthouse report shows; metrics carry a ready-formatted displayValue. The guide to reading a Lighthouse audit explains what each number means.
Setup
Before you start
One dependency, requests. Put your API key in the environment so it never lands in the file.
python3 -m pip install requests
export URLPIPE_API_KEY="your_api_key"
The request
Print the four scores, LCP, CLS and TBT
"sync": True holds the connection open until the result is ready and answers with it. Pass timeout= every time: requests has no default, and a page that never finishes loading would otherwise hang your process. Scores arrive from 0 to 1 and any of them can be None when Lighthouse could not compute it, so the loop checks before multiplying.
import os
import requests
res = requests.post(
"https://urlpipe.dev/lighthouse",
headers={"Authorization": f"Bearer {os.environ['URLPIPE_API_KEY']}"},
json={"url": "https://example.com", "device": "mobile", "sync": True},
# requests waits forever unless told otherwise; a sync call can take up to 60 s.
timeout=90,
)
res.raise_for_status()
report = res.json()
for name in ("performance", "accessibility", "best-practices", "seo"):
score = (report["categories"].get(name) or {}).get("score")
print(f"{name}: {'n/a' if score is None else round(score * 100)}")
metrics = {
"LCP": "largest-contentful-paint",
"CLS": "cumulative-layout-shift",
"TBT": "total-blocking-time",
}
for label, key in metrics.items():
print(f"{label}: {(report['metrics'].get(key) or {}).get('displayValue', 'n/a')}")
Run it: python3 lighthouse_audit.py
Given the example response on the docs page, it prints:
performance: 95
accessibility: 88
best-practices: 92
seo: 90
LCP: 2.5 s
CLS: 0.05
TBT: 150 msAsync
The async variant: a token, a webhook and a poll
Leave out sync and the answer is a token, straight away. for … else is the idiom for "ran out of attempts": the else runs only when the loop never hit break.
import os
import time
import requests
API = "https://urlpipe.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['URLPIPE_API_KEY']}"}
# No "sync": the request is accepted at once and the work carries on without you.
res = requests.post(
f"{API}/lighthouse",
headers=HEADERS,
json={
"url": "https://example.com",
"device": "mobile",
"report_to": "https://your-app.com/webhooks/urlpipe",
"labels": {"customer": "acme"},
},
timeout=30,
)
res.raise_for_status()
token = res.json()["token"]
print(f"Accepted {token}")
# The result is POSTed to report_to when it is ready. Polling by token is the
# other way to collect it: no endpoint needed, and a backup for the webhook.
for _ in range(60):
res = requests.get(f"{API}/result/{token}", headers=HEADERS, timeout=30)
if res.status_code != 202: # 202 means still processing
break
time.sleep(2)
else:
raise SystemExit("Still processing after two minutes; try the token again later.")
if res.status_code == 422:
raise SystemExit(f"The analysis failed: {res.json()['error']}")
if res.status_code == 410:
raise SystemExit("The result is past the 30-day window; send the request again.")
res.raise_for_status()
report = res.json()
for name in ("performance", "accessibility", "best-practices", "seo"):
score = (report["categories"].get(name) or {}).get("score")
print(f"{name}: {'n/a' if score is None else round(score * 100)}")
metrics = {
"LCP": "largest-contentful-paint",
"CLS": "cumulative-layout-shift",
"TBT": "total-blocking-time",
}
for label, key in metrics.items():
print(f"{label}: {(report['metrics'].get(key) or {}).get('displayValue', 'n/a')}")
Run it: python3 lighthouse_audit_async.py
Errors
Handle errors and retries
raise_for_status() is fine for a script; a service wants to tell the failures apart. Check the status before calling res.json() — a 401 body is not JSON. sys.exit(message) prints to stderr and exits 1.
import os
import sys
import time
import requests
API_KEY = os.environ["URLPIPE_API_KEY"]
class URLpipeError(Exception):
pass
def urlpipe(path, payload, attempts=5):
"""POST a sync request and return the response, or raise URLpipeError."""
for attempt in range(attempts):
res = requests.post(
f"https://urlpipe.dev{path}",
headers={"Authorization": f"Bearer {API_KEY}"},
json={**payload, "sync": True},
timeout=90,
)
if res.status_code == 200:
return res
if res.status_code == 401:
raise URLpipeError("401: the API key is missing or wrong. Check URLPIPE_API_KEY.")
try:
body = res.json()
except ValueError:
body = {}
code = body.get("error", "")
detail = f"{code}: {body['message']}" if body.get("message") else code
if res.status_code == 429 and code == "rate_limited":
# Sending too fast: Retry-After says how long the window has left.
time.sleep(int(res.headers.get("Retry-After", 1)))
elif res.status_code == 429 and code == "concurrency_limit":
# Every parallel slot on your plan is busy with your own requests.
time.sleep(2**attempt)
elif res.status_code == 504:
# Still running on our side; the token collects it from GET /result/:token.
raise URLpipeError(f"504 processing_timeout: collect it later with token {body['token']}")
else:
# 403 email_unverified, 422 (a bad parameter, or a page that would not load),
# 429 quota_exceeded: sending the same request again gets the same answer.
raise URLpipeError(f"{res.status_code}: {detail}")
raise URLpipeError(f"429: still refused after {attempts} attempts")
try:
res = urlpipe("/lighthouse", {"url": "https://example.com", "device": "mobile"})
except URLpipeError as error:
sys.exit(f"URLpipe: {error}")
except requests.RequestException as error:
sys.exit(f"Network error: {error}")
report = res.json()
for name in ("performance", "accessibility", "best-practices", "seo"):
score = (report["categories"].get(name) or {}).get("score")
print(f"{name}: {'n/a' if score is None else round(score * 100)}")
metrics = {
"LCP": "largest-contentful-paint",
"CLS": "cumulative-layout-shift",
"TBT": "total-blocking-time",
}
for label, key in metrics.items():
print(f"{label}: {(report['metrics'].get(key) or {}).get('displayValue', 'n/a')}")
Run it: python3 lighthouse_audit_errors.py
Details
What to know about /lighthouse
- Lighthouse 13 reports four categories; the
pwakey is alwaysnull, kept so the shape does not change. - The performance score weights TBT 30%, LCP 25%, CLS 25%, FCP 10% and Speed Index 10%.
- INP needs real users and cannot be measured in a lab run; TBT is its lab stand-in.
"include_audits": "true"adds the full list of audits, with the elements to fix. Mobile and desktop are cached separately.- Want the audit run on a schedule, with a history and alerts? Full Stack Audit sells that as a monitored report; URLpipe is the primitive underneath.
FAQ
Frequently asked questions
Mobile or desktop — which device should I audit?
Why isn't INP in the results?
Why use the async variant for Lighthouse?
Do I need an SDK to call URLpipe from Python?
Get a key and run it.
Free plan, no card. Paste your key into URLPIPE_API_KEY and every program on this page runs as it is.