Website Audit API
Start an audit with one POST, poll until it finishes, and get back the same versioned JSON schema the app exports. Available on the Agency plan.
On this page
The REST API runs the same analyzers the app does — 18 of them across SEO, security, accessibility, AI readiness, CSS, and hosting, with PageSpeed optional — and hands back the result as versioned JSON instead of a page. It is how you put audits inside your own onboarding flow, monitoring job, or client dashboard.
API access is an Agency-plan feature. Everything below is the public reference; creating and revoking keys happens on the API access page once you are signed in.
Authentication
Every request carries an API key in the Authorization header. Keys are created on the API access page; an Agency account can hold up to 5 active keys at a time.
Authorization: Bearer was_your_api_key_hereA key is the string was_ followed by 27 URL-safe characters, 31 in total. Only its hash is stored, so a key is shown once at creation and cannot be recovered afterwards — revoke and create a new one instead. Anything that is not a well-formed, active key belonging to an account with a live subscription is rejected with a 401 before any work is done.
Base URL
https://webauditsuite.com/api/v1Endpoints
Audits are asynchronous. You start one with a POST, which returns immediately with an id, then poll a GET until the status stops being processing. A full audit normally takes 10 to 30 seconds, or up to about two minutes when you ask for PageSpeed data.
/api/v1/audit
Starts an audit and returns 202 Accepted with the id you poll. It does not return results.
Request Body
{
"url": "https://example.com",
"include_psi": false
}url— required. A publichttporhttpsURL, 2048 characters or fewer. Addresses that resolve to private, loopback, or cloud-metadata ranges are refused.include_psi— optional, defaults tofalse. When true the audit also runs Google PageSpeed Insights, which adds up to 90 seconds. When it is left off, thepagespeedtab comes back empty with anunrunStaterather than missing.
Response (202)
{
"id": "audit_1785086404956_k3f9qa",
"status": "processing",
"_links": {
"self": "/api/v1/audit/audit_1785086404956_k3f9qa",
"poll": "/api/v1/audit/audit_1785086404956_k3f9qa"
}
}self and poll are the same URL today — follow poll, so a future split does not break you.
/api/v1/audit/{id}
Retrieves an audit by id. Results are kept for one hour after the audit completes, then the id returns NOT_FOUND. Polling does not consume any rate limit.
While it runs
{
"id": "audit_1785086404956_k3f9qa",
"status": "processing",
"_links": {
"self": "/api/v1/audit/audit_1785086404956_k3f9qa",
"poll": "/api/v1/audit/audit_1785086404956_k3f9qa"
}
}When it finishes
Abridged below — the eight tabs and their cards are elided, everything else is a real response for https://example.com. That site sends no security headers at all, which is why its security score really is 0.
{
"id": "audit_1785086404956_k3f9qa",
"status": "complete",
"schemaVersion": "1.6.0",
"exportedAt": "2026-07-26T17:20:04.957Z",
"metadata": {
"url": "https://example.com",
"analyzedAt": "2026-07-26T17:20:04.956Z",
"psiCompleted": false,
"userAuthenticated": true
},
"scores": {
"accessibility": { "value": 70, "locked": true, "wcagLevel": "Partial A" },
"geo": { "value": 20, "locked": true },
"security": { "value": 0, "locked": true },
"css": { "value": 100, "locked": true },
"seo": {
"compositeScore": {
"value": 54, "maxPossible": 100, "percentage": 54, "locked": false
}
},
"brokenLinks": { "value": 100, "locked": false },
"redirects": { "value": 100, "locked": false },
"canonicals": { "value": 95, "locked": false },
"metadata": { "value": 83, "locked": false },
"sitemap": { "value": 50, "locked": false }
},
"tabs": {
"overview": { "enabled": true, "order": 1, "cards": { ... } },
"pagespeed": { "enabled": true, "order": 2, "locked": false, "cards": {},
"unrunState": { "show": true,
"message": "PageSpeed Insights has not been run yet" } },
"accessibility": { "enabled": true, "order": 3, "cards": { ... } },
"seo": { "enabled": true, "order": 4, "cards": { ... } },
"geo": { "enabled": true, "order": 5, "cards": { ... } },
"security": { "enabled": true, "order": 6, "cards": { ... } },
"css": { "enabled": true, "order": 7, "cards": { ... } },
"hosting": { "enabled": true, "order": 8, "cards": { ... } }
},
"_links": {
"self": "/api/v1/audit/audit_1785086404956_k3f9qa",
"expiresAt": "2026-07-26T18:20:04.958Z"
}
}When it fails
A failed audit is still HTTP 200 — the failure is in the body, not the status code. An audit still processing five minutes after it started is reported the same way, with "Audit timed out".
{
"id": "audit_1785086404956_k3f9qa",
"status": "failed",
"error": "Failed to fetch the target URL. The site may be unreachable, blocking automated requests, or timing out."
}/api/v1/usage
Current usage and remaining quota for the key that made the request. Like polling, this endpoint does not consume any rate limit.
{
"key": {
"id": "8f1c0d2e-4a55-4a1b-9f3c-6d0e2b7a91c4",
"name": "Production Server",
"prefix": "was_9tKq3bXd",
"createdAt": "2026-07-01T09:12:44.000Z",
"lastUsedAt": "2026-07-26T17:20:04.000Z",
"totalRequests": 42
},
"tier": "agency",
"limits": {
"burst": { "limit": 2, "used": 1, "remaining": 1, "resetsIn": "1 minute" },
"daily": { "limit": 25, "used": 12, "remaining": 13, "resetsAt": "2026-07-27T00:00:00.000Z" },
"monthly": { "limit": 100, "used": 42, "remaining": 58, "resetsAt": "2026-08-01T00:00:00.000Z" }
}
}Code Examples
The same start-then-poll flow in three languages. Poll on an interval you are comfortable with — reads are free — and always branch on status before reading scores. The shell samples read the key from $WAS_API_KEY; keep it in your environment rather than in shell history or a committed file.
cURL
# 1. Start the audit — returns 202 with an id
curl -X POST https://webauditsuite.com/api/v1/audit \
-H "Authorization: Bearer $WAS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'
# 2. Poll until status stops being "processing"
curl https://webauditsuite.com/api/v1/audit/audit_1785086404956_k3f9qa \
-H "Authorization: Bearer $WAS_API_KEY"JavaScript
const API = 'https://webauditsuite.com/api/v1';
const auth = { Authorization: 'Bearer was_your_api_key' };
const started = await fetch(`${API}/audit`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com' }),
});
const { id } = await started.json(); // 202, status: "processing"
let audit;
do {
await new Promise((resolve) => setTimeout(resolve, 5000));
audit = await fetch(`${API}/audit/${id}`, { headers: auth }).then((r) => r.json());
} while (audit.status === 'processing');
if (audit.status === 'failed') throw new Error(audit.error);
console.log(audit.scores);Python
import time
import requests
API = 'https://webauditsuite.com/api/v1'
auth = {'Authorization': 'Bearer was_your_api_key'}
started = requests.post(
f'{API}/audit',
headers=auth,
json={'url': 'https://example.com'},
)
audit_id = started.json()['id'] # 202, status: "processing"
while True:
time.sleep(5)
audit = requests.get(f'{API}/audit/{audit_id}', headers=auth).json()
if audit['status'] != 'processing':
break
if audit['status'] == 'failed':
raise RuntimeError(audit['error'])
print(audit['scores'])Rate Limits
Only POST /api/v1/audit is metered. Reading an audit or checking usage is free, so polling costs you nothing.
| Limit Type | Agency Tier | Reset |
|---|---|---|
| Burst | 2 requests/minute | Every minute |
| Daily | 25 requests/day | Midnight UTC |
| Monthly | 100 requests/month | 1st of month UTC |
Every audit request counts toward the daily and monthly limits — including requests we reject for an invalid body, an unsupported URL, or rate limiting. An integration that retries on error will use up its allowance, so handle error responses rather than looping on them.
A completed API audit is also written to your scan history, so it draws on the same monthly allowance as audits you run in the app — 100 in total on Agency. Exhausting it returns QUOTA_EXCEEDED even when the per-key counters still have room.
Errors
Errors return a consistent JSON structure. error is the human-readable message and code is the stable identifier — branch on code, never on the message text.
{
"error": "Daily limit exceeded. Resets at midnight UTC.",
"code": "DAILY_LIMITED",
"remaining": { "burst": 1, "daily": 0, "monthly": 58 },
"resetAt": "2026-07-27T00:00:00.000Z"
}remaining and resetAt appear on rate-limit rejections; details appears when a request body failed validation. Those responses also carry Retry-After (60 seconds for a burst rejection, 3600 otherwise) and X-RateLimit-Remaining, which a successful 202 carries too.
| Code | HTTP | Message |
|---|---|---|
| INVALID_KEY | 401 | Invalid API key |
| KEY_REVOKED | 401 | API key has been revoked |
| KEY_EXPIRED | 401 | API key has expired |
| SUBSCRIPTION_INACTIVE | 401 | Subscription is not active |
| INVALID_REQUEST | 400 | Invalid request format |
| INVALID_URL | 400 | Invalid URL provided |
| NOT_FOUND | 404 | Resource not found |
| BURST_LIMITED | 429 | Too many requests. Please wait a moment. |
| DAILY_LIMITED | 429 | Daily limit exceeded. Resets at midnight UTC. |
| MONTHLY_LIMITED | 429 | Monthly limit exceeded. Upgrade for more requests. |
| QUOTA_EXCEEDED | 429 | Monthly scan quota exceeded. Upgrade your plan for more scans. |
| INTERNAL_ERROR | 500 | Internal server error |
An audit that fails after it starts never appears here: GET /api/v1/audit/{id} returns HTTP 200 with status: "failed" and a plain-English error string.
Response Schema & Versioning
A completed audit carries schemaVersion — currently 1.6.0 — using the same versioned structure as the JSON export you can download from the app. Check that field first. New optional fields arrive in minor bumps and existing fields keep their meaning, so a consumer written against any 1.x response keeps working; a major bump is the only thing that can remove or rename a field.
scores holds every computed score, and tabs holds the eight result tabs — overview, pagespeed, accessibility, seo, geo, security, css, and hosting — each at least an { enabled, order, cards } object whose cards hold the detailed findings. Website audit scores explained covers what each number measures, and reports and exports covers the export the API mirrors.
Two differences from a downloaded export, worth knowing before you write a shared parser: the API adds id, status, and _links around the audit, and it does not return the optional aiSuggestions or failures blocks a download can include. A completed audit response carries exactly schemaVersion, exportedAt, metadata, scores, and tabs.
Next Steps
- Reports & exports — the JSON export this API mirrors, field by field, plus the white-label PDF the same audit can become.
- The 18 website audit checks we run — what every analyzer behind these scores actually inspects.
- Website audit report example — the same data rendered the way a client reads it.