PageSpeedAudit Developers · Error catalog

Every error names what to do next.

Every refusal the API makes on purpose is RFC 9457 problem+json. Branch on code — it is stable — and never on detail, which is free to improve. The type on every such response is a link to its row on this page, so the error in your terminal is one click from its recovery. Three answers carry no body: a bare 401, 429 and 500.

{
  "type": "https://pagespeedaudit.com/docs/errors#runs_exhausted",
  "title": "Conflict",
  "status": 409,
  "detail": "All included runs have been used.",
  "code": "runs_exhausted"
}

43 codes

Error codes by status

400 Bad request — the input is wrong

url_requiredHTTP 400

When
The first run of an audit was started without a URL.
Recovery
Pass url on the first run; it is locked to the audit from then on.
#

invalid_formatHTTP 400

When
format was something other than json or html.
Recovery
Request format=json for the findings document or format=html for the customer report.
#

invalid_quantityHTTP 400

When
quantity is below 1 or above the per-order maximum.
Recovery
Order between 1 and the maximum named in detail; split larger volumes into several orders.
#

idempotency_key_requiredHTTP 400

When
A real order was placed without an Idempotency-Key header (or idempotencyKey in the body).
Recovery
Generate one key per intended order (a UUID is fine) and send it on every retry of that order. A dry run needs none.
#

unknown_scopeHTTP 400

When
A key was requested with a scope that does not exist.
Recovery
Use only read, run and purchase.
#

unusable_emailHTTP 400

When
The checkout email cannot receive the audit (malformed, or a disposable address).
Recovery
Ask the buyer for a deliverable address, or omit email and let Checkout collect it.
#

invalid_dateHTTP 400

When
from or to is not an ISO 8601 date.
Recovery
Send dates as 2026-07-01 or 2026-07-01T00:00:00Z.
#

invalid_group_byHTTP 400

When
groupBy was something other than subid or campaign.
Recovery
Use groupBy=subid, groupBy=campaign, or omit it for totals only.
#

invalid_statusHTTP 400

When
status was not one of the referral states.
Recovery
Filter with pending, approved, reversed or paid, or omit it.
#

invalid_postback_eventsHTTP 400

When
events named an event the postback cannot fire on.
Recovery
Use any of sale, approved, reversed; omit the list for sale only.
#

invalid_postback_urlHTTP 400

When
The postback URL is not https, not on a public host, or otherwise unusable — detail says which.
Recovery
Point the postback at a public https endpoint; send an empty url to clear it.
#

disposable_emailHTTP 400

When
The address belongs to a temporary inbox.
Recovery
Use the address that is actually read; the newsletter never reaches a disposable one.
#

invalid_messageHTTP 400

When
The message is empty or longer than 5000 characters.
Recovery
Send a message of 1–5000 characters.
#

402 Payment required — the saved card was refused

authentication_requiredHTTP 402

When
The saved card's bank wants the cardholder present (3-D Secure). The problem carries the recorded order's orderId and a checkoutUrl.
Recovery
Open the checkoutUrl from the problem body in a browser to finish the payment; poll GET /orders/{orderId} for completed. Nothing was charged.
#

card_declinedHTTP 402

When
The saved card was declined, or the charge did not complete — detail says which. The problem carries the recorded order's orderId.
Recovery
If detail says declined, nothing was charged: ask the account holder to update the card under Billing, then order again with a new Idempotency-Key. If detail says the charge did not complete, the outcome is unknown: read GET /orders/{orderId} before retrying.
#

403 Forbidden — this credential may not do this

insufficient_scopeHTTP 403

When
The API key does not carry the scope the operation needs; detail names it.
Recovery
Create a key with that scope at /account/api-keys. purchase is never implicit.
#

api_key_requiredHTTP 403

When
A signed-in browser session, not an API key, called the ordering operations — including the request console on the reference page.
Recovery
Order with an API key created with the purchase scope, or buy on /pricing. A session is deliberately never enough to place an order here.
#

customers_onlyHTTP 403

When
The Target Finder was called from an account with no active audit — none bought yet, or every one expired or revoked.
Recovery
It unlocks with an active audit: buy one on /pricing or place an order, then analyze before the first run locks the URL.
#

cross_site_requestHTTP 403

When
A state-changing call authenticated by the browser session cookie arrived from another origin. API-key and same-origin callers never see this.
Recovery
Send an X-Api-Key credential — the supported way to call this API from anywhere — or make the request from this site's own origin. This is a cross-site request forgery guard, not a permission check: the credential is fine, the origin is not.
#

404 Not found — or not yours

not_foundHTTP 404

When
The audit, run, order, analysis or key does not exist — or belongs to another account; the two answers are identical by design. A run's status, timeline and report also answer this once the audit behind it was revoked after a refund or chargeback.
Recovery
Check the id against your own GET /audits, GET /orders or GET /target-finder/analyses listings; an audit listed as Revoked has no readable runs.
#

report_not_readyHTTP 404

When
The run has no report yet — it has not succeeded, or it failed.
Recovery
Poll GET /runs/{runId} until status is Succeeded; a Failed or TimedOut run has no report and did not consume a run.
#

account_not_foundHTTP 404

When
The account behind the key could not be loaded, or it has no email address on file to attach an order to.
Recovery
Sign in, check the account and its email under settings; if it persists, contact support with the key prefix.
#

409 Conflict — the resource's state refuses this

url_lockedHTTP 409

When
A re-run was requested with a different URL than the first run locked.
Recovery
Re-runs must reuse the locked URL; omit url or send the same one. A different page needs its own audit.
#

run_in_progressHTTP 409

When
A run is already queued or running for this audit.
Recovery
Poll the active run; do not start another. This is what makes a retried POST safe.
#

runs_exhaustedHTTP 409

When
All included runs (the first plus every re-run) have been used.
Recovery
Buy another audit, or an extra run from the dashboard.
#

revokedHTTP 409

When
The audit was revoked — its purchase was refunded or charged back.
Recovery
Nothing to retry; a new purchase creates a new entitlement.
#

idempotency_key_reusedHTTP 409

When
The Idempotency-Key was already used for an order with different parameters.
Recovery
Use a fresh key for a different order; replaying the same key with the same parameters returns the original order.
#

wholesale_unavailableHTTP 409

When
The account is an approved reseller but its wholesale code could not be applied — the order was refused rather than billed at list price.
Recovery
Nothing was created or charged; contact support to repair the code, then retry.
#

account_email_missingHTTP 409

When
The account's stored email address is not usable for an order receipt.
Recovery
Correct the email under account settings, then retry.
#

url_already_lockedHTTP 409

When
Every audit on the account already has its URL locked, so there is nothing for a recommendation to steer.
Recovery
Analyze before the first run of a new audit; past analyses stay readable.
#

duplicate_key_nameHTTP 409

When
An active key already uses this name.
Recovery
Choose another name, or revoke the old key first.
#

key_limit_reachedHTTP 409

When
The account is at its maximum number of active keys.
Recovery
Revoke a key you no longer use, then create the new one.
#

429 Too many requests — slow down

daily_limit_reachedHTTP 429

When
The account reached its daily maximum of orders or committed spend — the same ceiling for every account.
Recovery
Wait for the window to reset at 00:00 UTC; split large volumes across days.
#

analysis_limit_reachedHTTP 429

When
The account's daily Target Finder allowance is used.
Recovery
Try again tomorrow; past analyses stay readable meanwhile.
#

502 Bad gateway — an upstream call failed

checkout_unavailableHTTP 502

When
Stripe could not start a Checkout session.
Recovery
Retry after a short wait; nothing was created.
#

503 Service unavailable — switched off or paused

ordering_disabledHTTP 503

When
Ordering through the API is switched off.
Recovery
Order on the website meanwhile; the switch is operational, not account-specific.
#

ordering_pausedHTTP 503

When
The platform-wide daily ordering backstop tripped.
Recovery
Retry shortly; nothing was charged.
#

target_finder_disabledHTTP 503

When
The Target Finder is switched off.
Recovery
Choose the URL yourself for now; the audit itself is unaffected.
#

Without a code the answers no handler writes

HTTP 401empty body

When
The X-Api-Key header is missing, malformed, revoked, or the account is blocked. Empty body.
Recovery
Send a live key from /account/api-keys in X-Api-Key. A revoked key never works again — create a new one.

HTTP 429empty body

When
The rate limit is exceeded: 60 requests per minute per account on authenticated operations, 10 per minute per IP on anonymous ones, 5 per minute to POST /orders (dry runs included). Empty body.
Recovery
Back off and retry; poll a run every few seconds, not continuously.

HTTP 500empty body

When
An unhandled failure — nothing the API refused on purpose. Empty body.
Recovery
Do not retry blindly. After a 500 from POST /orders, read GET /orders first: the outcome is unknown and a charge may have completed. Elsewhere, retry once after a short wait, then contact support with the time and path.

How to branch

switch (problem.code) {
  case "run_in_progress":   // poll the active run; do not restart
  case "runs_exhausted":    // buy another audit or an extra run
  case "url_locked":        // reuse the locked URL
  case "insufficient_scope": // create a key with the named scope
  case "daily_limit_reached": // wait for 00:00 UTC
  default:                  // surface status + detail; never retry blindly
}

Three rules that keep an integration safe

  • 404 means "not yours" as often as "not there". Ownership failures are deliberately indistinguishable; check the id against your own listings.
  • Every 4xx, the 503s and the 502 charged nothing — with one named exception: a card_declined whose detail says the charge did not complete. On a 500 from POST /orders the outcome is unknown: read GET /orders before retrying.
  • 409 on a POST is the API refusing a duplicate, not asking for a retry. run_in_progress and idempotency_key_reused exist so a retried request can never spend twice.