The PWA Today Runtime Audit API allows trial and subscribed customers to create audits,
retrieve results, and list application history without using the
@pwa-today/pwa-check CLI.
Most CI/CD integrations should use the CLI because it handles authentication, polling, reports, quality-gate exit codes, and service worker deployment coordination. Use the API directly when building a custom integration.
Download the OpenAPI 3.1 specification.
Related documentation:
- Integrating PWA Today Audits into CI/CD
pwa-check.ymlConfiguration Reference- Service Worker Deployment Testing
Base URL
https://api.pwa.today
All request and response bodies described below use JSON unless stated otherwise.
Authentication
Get an access token
Send the customer client ID and client secret with HTTP Basic authentication:
POST /token
Authorization: Basic BASE64(CLIENT_ID:CLIENT_SECRET)
With curl:
curl --silent --show-error --fail \
--user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
--request POST \
https://api.pwa.today/token
Example OAuth response:
{
"access_token": "eyJraWQiOi...",
"expires_in": 3600,
"token_type": "Bearer"
}
Use the values returned by the API. Clients must not assume a fixed token lifetime.
For shell-based integrations:
export PWA_AUDIT_TOKEN="$(
curl --silent --show-error --fail \
--user "$PWA_CLIENT_ID:$PWA_CLIENT_SECRET" \
--request POST \
https://api.pwa.today/token |
node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).access_token'
)"
Authenticate API requests
Send the access token as a bearer token:
Authorization: Bearer ACCESS_TOKEN
Example:
curl --silent --show-error --fail \
--header "Authorization: Bearer $PWA_AUDIT_TOKEN" \
https://api.pwa.today/v1/applications/example.com/audits
Do not put the client secret or access token in URLs, request bodies, audit options, source metadata, logs, or artifacts.
Application prerequisite
Add the audited HTTPS origin under Applications in the customer console and verify its hostname with the supplied DNS record or meta tag. An audit URL must use that Application’s hostname. One Application can have multiple audit configurations and its audits share one history.
Plans, audit coverage, and allowances
Every confirmed account starts with three complimentary Standard runtime audits for one application. This is a lifetime allowance and does not require a payment method.
| Plan | Accepted audit | Allowance | Applications |
|---|---|---|---|
| Complimentary trial | Standard runtime audit | 3 lifetime | 1 |
| Developer | Standard runtime audit | 100 per billing period | 3 |
| Team | Full-suite selection or complete release audit | 250 per billing period | 15 |
| Business | Full-suite selection or complete release audit | 500 per billing period | 50 |
A Standard runtime audit uses the standard profile and may exclude
Standard checks, provided at least one check remains selected. It cannot include
checks outside the Standard profile. Trial and Developer accept only this audit
type. Team and Business dashboard configurations may also include any check from
the Full runtime suite except service-worker-deployment. They also accept a
complete release audit, which uses the exact full profile with
service-worker-deployment included and a deployment testId. The
deployment validation and Full runtime suite form one audit and consume one
allowance.
Paid allowances reset at the start of each Stripe billing period, not at the start of a calendar month. Manage payment details, invoices, plan changes, and cancellation from the Billing section of the account dashboard. A canceled subscription remains available until the displayed paid-period end.
Audit lifecycle
An aggregate audit is asynchronous:
- Create an audit with
POST /v1/audits. - Poll
GET /v1/audits/{auditId}. - Stop polling when the status is terminal.
- Retrieve ordered check results from
GET /v1/audits/{auditId}/results.
Audit statuses:
| Status | Terminal | Meaning |
|---|---|---|
queued |
No | The audit was accepted and is waiting to run. |
running |
No | Runtime checks are executing. |
completed |
Yes | Checks completed without infrastructure errors. The quality gate may still have failed. |
partially-completed |
Yes | Some checks completed and at least one had an infrastructure error. |
failed |
Yes | The audit could not run successfully. |
An audit’s execution status and quality gate are separate. A completed audit
can have qualityGate.passed: false.
Create an audit
POST /v1/audits
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
Minimal request:
{
"url": "https://example.com"
}
Complete request example:
{
"url": "https://example.com",
"applicationId": "example.com",
"profile": "standard",
"include": [],
"exclude": [],
"options": {
"offline-navigation": {
"series": [
["/", "/products", "/checkout"],
["/", "/account"]
],
"expectedSelectors": [
"main"
]
}
},
"qualityGate": {
"minimumScore": 90,
"failOn": [
"critical",
"high"
]
},
"source": {
"type": "ci",
"provider": "bitbucket",
"repository": "acme/storefront",
"branch": "main",
"commit": "a1b2c3d4",
"pipelineUrl": "https://ci.example.com/builds/123",
"environment": "production"
}
}
Request fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
url |
String | Yes | None | HTTP or HTTPS URL to audit. URL fragments are removed. |
applicationId |
String | No | Audited hostname | Application hostname. It must match the audit URL and an Application in the customer account. |
profile |
String | No | standard |
quick, standard, full, or custom. |
include |
Array of strings | No | [] |
Additional stable check IDs. For custom, this is the complete check set. |
exclude |
Array of strings | No | [] |
Check IDs removed from the selected set. |
options |
Object | No | {} |
Per-check options keyed by stable check ID. |
qualityGate |
Object | No | Score 90, severities critical and high |
Quality-gate configuration. |
source |
Object | No | {} |
Optional source-control and CI metadata. |
See the pwa-check.yml Configuration Reference
for profile contents, check IDs, and per-check options. The YAML audit
object maps directly to this JSON request, except that local deployment fields
such as command and commandTimeout must never be sent to the API.
The API enforces the audit configurations included with the customer’s plan.
It does not permit quick, custom, or checks outside the Standard
profile for dashboard audits. Team and Business may run either a selectable
Standard audit or an exact complete release audit.
Source fields
The API accepts these optional string fields under source:
typeproviderrepositorybranchcommitpullRequestpipelineUrlenvironmentcliVersion
Each value is limited to 2048 characters. Unsupported fields and non-string values are omitted.
Response
Successful creation returns HTTP 202:
{
"auditId": "36a14af6-e488-475d-8c4e-ae262e5bff99",
"status": "queued",
"reused": false,
"statusUrl": "/v1/audits/36a14af6-e488-475d-8c4e-ae262e5bff99",
"resultsUrl": "/v1/audits/36a14af6-e488-475d-8c4e-ae262e5bff99/results"
}
statusUrl and resultsUrl are relative to the API base URL.
Idempotent audit creation
Use the Idempotency-Key header when a pipeline may retry the creation
request:
curl --silent --show-error --fail \
--request POST \
--header "Authorization: Bearer $PWA_AUDIT_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $CI_PIPELINE_ID" \
--data '{"url":"https://example.com"}' \
https://api.pwa.today/v1/audits
The key:
- Is scoped to the authenticated customer.
- Must not exceed 200 characters.
- Produces the same audit ID when reused.
A repeated request returns HTTP 202 with:
{
"auditId": "audit_...",
"status": "running",
"reused": true,
"statusUrl": "/v1/audits/audit_...",
"resultsUrl": "/v1/audits/audit_.../results"
}
Use one key for one logical audit run. Reusing a key with different request content still returns the original audit rather than creating a replacement.
Get audit status
GET /v1/audits/{auditId}
Authorization: Bearer ACCESS_TOKEN
Example:
curl --silent --show-error --fail \
--header "Authorization: Bearer $PWA_AUDIT_TOKEN" \
"https://api.pwa.today/v1/audits/$AUDIT_ID"
Example terminal response:
{
"auditId": "36a14af6-e488-475d-8c4e-ae262e5bff99",
"applicationId": "example.com",
"url": "https://example.com/",
"status": "completed",
"profile": "full",
"selectedChecks": [
"manifest",
"offline",
"offline-navigation"
],
"score": 100,
"qualityGate": {
"minimumScore": 90,
"failOn": [
"critical",
"high"
],
"passed": true,
"failedSeverities": []
},
"source": {
"type": "ci",
"provider": "bitbucket",
"commit": "a1b2c3d4"
},
"versions": {
"cliVersion": null,
"apiVersion": "v1",
"engineVersion": "1.0.0",
"rulesetVersion": "2026.08",
"engineRevision": null
},
"createdAt": "2026-07-29T08:00:00.000Z",
"startedAt": "2026-07-29T08:00:03.000Z",
"completedAt": "2026-07-29T08:01:12.000Z",
"error": null
}
While an audit is queued or running, score, startedAt, and completedAt
may be null, and qualityGate does not yet contain the final passed value.
An audit ID belonging to another customer is returned as not found.
Get audit results
GET /v1/audits/{auditId}/results
Authorization: Bearer ACCESS_TOKEN
Example:
curl --silent --show-error --fail \
--header "Authorization: Bearer $PWA_AUDIT_TOKEN" \
"https://api.pwa.today/v1/audits/$AUDIT_ID/results"
Response:
{
"auditId": "36a14af6-e488-475d-8c4e-ae262e5bff99",
"status": "completed",
"terminal": true,
"results": [
{
"check": "manifest",
"status": "passed",
"severity": "critical",
"message": "Manifest and image validation completed.",
"details": {},
"startedAt": "2026-07-29T08:00:03.000Z",
"completedAt": "2026-07-29T08:00:07.500Z",
"durationMs": 4500
}
]
}
Results are returned in execution order. The endpoint can be called while the
audit is running; in that case it returns the results persisted so far and
terminal: false.
Check result statuses:
passedfailedwarningnot-applicableskippederror
List audits for an application
GET /v1/applications/{applicationId}/audits
Authorization: Bearer ACCESS_TOKEN
Example:
curl --silent --show-error --fail \
--header "Authorization: Bearer $PWA_AUDIT_TOKEN" \
"https://api.pwa.today/v1/applications/example.com/audits?limit=20"
Response:
{
"applicationId": "example.com",
"audits": [
{
"auditId": "36a14af6-e488-475d-8c4e-ae262e5bff99",
"applicationId": "example.com",
"url": "https://example.com/",
"status": "completed",
"profile": "full",
"selectedChecks": [
"manifest",
"offline"
],
"score": 100,
"qualityGate": {
"minimumScore": 90,
"failOn": [
"critical",
"high"
],
"passed": true,
"failedSeverities": []
},
"source": {},
"versions": {
"apiVersion": "v1",
"engineVersion": "1.0.0",
"rulesetVersion": "2026.08"
},
"createdAt": "2026-07-29T08:00:00.000Z",
"startedAt": "2026-07-29T08:00:03.000Z",
"completedAt": "2026-07-29T08:01:12.000Z",
"error": null
}
]
}
limit defaults to 20, with a minimum of 1 and maximum of 100. Results
are returned newest first and are scoped to the authenticated customer and
application ID.
Advanced: service worker deployment API
The CLI is strongly recommended for service worker deployment checks. A direct integration must implement a stateful coordination protocol and must cancel the test when deployment cannot continue.
Starting a deployment check requires an active Team or Business entitlement.
For CLI-managed Vercel, Netlify, and AWS Amplify Hosting deployments, see Service Worker Deployment Testing.
The deployment check is separate from the aggregate audit because it must start
before deployment and finish afterward. Its completed result is then imported
into the aggregate audit by testId.
Deployment states
| State | Terminal | Meaning |
|---|---|---|
starting |
No | The hosted browser task is starting. |
establishing-baseline |
No | The current service worker and application are being captured. |
baseline-ready |
No | The caller may now deploy. |
deployment-reported |
No | The caller reported that deployment finished. |
completed |
Yes | The deployment check passed or was not applicable. |
failed |
Yes | Deployment verification failed. |
cancelled |
Yes | The caller cancelled the check. |
Only one active deployment check can exist for a URL. A concurrent start
returns HTTP 409.
1. Start the deployment check
POST /checks/serviceworker-deployment
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json
Request:
{
"url": "https://example.com",
"deploymentTimeout": 900000,
"navigationTimeout": 20000,
"requiredCachedUrls": [
"/",
"/offline"
],
"contentCheckUrls": [
"/",
"/app.js"
]
}
Do not send a local deployment command to this endpoint. Direct API clients run their deployment themselves after the baseline becomes ready.
Successful creation returns HTTP 202:
{
"testId": "1f3af63f-36d8-45f7-83af-ba7306883793",
"state": "starting",
"deploymentToken": "TEST_SPECIFIC_SECRET",
"statusUrl": "/checks/serviceworker-deployment/1f3af63f-36d8-45f7-83af-ba7306883793",
"completionUrl": "/checks/serviceworker-deployment/1f3af63f-36d8-45f7-83af-ba7306883793/complete"
}
Keep deploymentToken private. It is separate from the customer access token
and authorizes state changes for this one deployment test.
2. Wait for the baseline
Poll the returned statusUrl with both tokens:
GET /checks/serviceworker-deployment/{testId}
Authorization: Bearer ACCESS_TOKEN
X-Deployment-Token: TEST_SPECIFIC_SECRET
Example:
curl --silent --show-error --fail \
--header "Authorization: Bearer $PWA_AUDIT_TOKEN" \
--header "X-Deployment-Token: $DEPLOYMENT_TOKEN" \
"https://api.pwa.today/checks/serviceworker-deployment/$TEST_ID"
Status response:
{
"testId": "1f3af63f-36d8-45f7-83af-ba7306883793",
"state": "baseline-ready",
"url": "https://example.com/",
"createdAt": "2026-07-29T08:00:00.000Z",
"updatedAt": "2026-07-29T08:00:30.000Z",
"baseline": {
"capturedAt": "2026-07-29T08:00:29.000Z",
"scope": "https://example.com/",
"scriptURL": "https://example.com/service-worker.js",
"workerHash": "...",
"documentHash": "..."
},
"deployment": null,
"result": null,
"error": null
}
Do not start deployment until state is baseline-ready. Stop and handle the
error if the state becomes failed or cancelled.
3. Deploy and wait
Start deployment of the commit being audited. Do not continue until the hosting provider confirms that deployment is complete and the new version is publicly observable.
The deployed service worker file must differ from the baseline service worker.
4. Report deployment completion
POST /checks/serviceworker-deployment/{testId}/complete
Authorization: Bearer ACCESS_TOKEN
X-Deployment-Token: TEST_SPECIFIC_SECRET
Content-Type: application/json
Optional metadata:
{
"deploymentId": "hosting-job-123",
"commitSha": "a1b2c3d4"
}
Successful reporting returns HTTP 202:
{
"testId": "1f3af63f-36d8-45f7-83af-ba7306883793",
"state": "deployment-reported"
}
If the baseline is not ready, the endpoint returns HTTP 409.
5. Wait for a terminal result
Continue polling the deployment status endpoint until the state is
completed, failed, or cancelled.
Example completed response:
{
"testId": "1f3af63f-36d8-45f7-83af-ba7306883793",
"state": "completed",
"url": "https://example.com/",
"createdAt": "2026-07-29T08:00:00.000Z",
"updatedAt": "2026-07-29T08:03:00.000Z",
"baseline": {},
"deployment": {
"deploymentId": "hosting-job-123",
"commitSha": "a1b2c3d4"
},
"result": {
"check": "serviceworker-deployment",
"status": "passed",
"message": "The service worker deployment completed successfully.",
"details": {}
},
"error": null
}
Use the returned result even when the deployment state is failed; the
aggregate audit can import it as a failed check.
6. Import the result into an aggregate audit
Create the aggregate audit with the deployment check explicitly included and
the completed testId:
curl --silent --show-error --fail \
--request POST \
--header "Authorization: Bearer $PWA_AUDIT_TOKEN" \
--header "Content-Type: application/json" \
--data "{
\"url\": \"https://example.com\",
\"applicationId\": \"example.com\",
\"profile\": \"full\",
\"include\": [\"service-worker-deployment\"],
\"options\": {
\"service-worker-deployment\": {
\"testId\": \"$TEST_ID\"
}
}
}" \
https://api.pwa.today/v1/audits
The deployment test must:
- Belong to the authenticated customer.
- Match the normalized audit URL.
- Be in
completedorfailedstate. - Contain a check result.
For direct aggregate-audit requests,
options.service-worker-deployment may contain only testId.
Cancel a deployment check
If deployment fails, times out, or cannot start, cancel the test:
DELETE /checks/serviceworker-deployment/{testId}
Authorization: Bearer ACCESS_TOKEN
X-Deployment-Token: TEST_SPECIFIC_SECRET
Example:
curl --silent --show-error --fail \
--request DELETE \
--header "Authorization: Bearer $PWA_AUDIT_TOKEN" \
--header "X-Deployment-Token: $DEPLOYMENT_TOKEN" \
"https://api.pwa.today/checks/serviceworker-deployment/$TEST_ID"
Response:
{
"testId": "1f3af63f-36d8-45f7-83af-ba7306883793",
"state": "cancelled"
}
Cancellation releases the URL lock. Calling it for an already-terminal test returns the existing terminal state.
Use a finally block, trap, or equivalent cleanup mechanism so an interrupted
custom integration does not leave a deployment check active until timeout.
HTTP status codes
| Status | Meaning |
|---|---|
200 |
Request completed successfully. |
202 |
Asynchronous work was accepted or a deployment signal was recorded. |
400 |
Invalid request, configuration, state reference, or idempotency key. |
401 |
Missing, expired, or invalid credentials or deployment token. |
403 |
The customer is unauthorized or the plan cannot run paid audits. |
404 |
The resource does not exist or does not belong to the customer. |
409 |
Conflicting deployment check or deployment state transition. |
429 |
The current audit allowance has been used. |
500 |
Unexpected service failure. |
502 |
The token endpoint could not reach the authorization service. |
503 |
The runtime audit worker could not be started. |
Billing and limit errors use a stable code and an upgrade URL:
{
"code": "AUDIT_LIMIT_REACHED",
"error": "Your audit allowance has been used.",
"upgradeUrl": "https://pwa.today/pricing/"
}
| Code | Meaning |
|---|---|
ACCOUNT_REQUIRED | No canonical customer account is available. |
SUBSCRIPTION_REQUIRED | Billing is inactive, ended, or otherwise unavailable. |
PAYMENT_PAST_DUE | Payment is past due and the grace period has ended. |
AUDIT_TYPE_NOT_ALLOWED | The requested profile is not included in the plan. |
RELEASE_DEPLOYMENT_REQUIRED | A complete release audit requires deployment validation. |
AUDIT_LIMIT_REACHED | The audit allowance for the lifetime trial or billing period is exhausted. |
APPLICATION_REQUIRED | Add the audited hostname as an Application before running an audit. |
APPLICATION_NOT_VERIFIED | Verify the Application before running an audit. |
APPLICATION_LIMIT_REACHED | The plan’s Application allowance is exhausted. |
AUDIT_RESERVATION_CONFLICT | Account state changed during an atomic reservation; retry with the same idempotency key. |
Other errors may contain only a human-readable error field.
Some asynchronous-start errors also include the generated auditId or
testId.
Clients should:
- Treat
400,401, and403as configuration, authentication, or billing failures. - Treat
429as an allowance limit and do not retry the same request automatically. - Retry transient
500,502, and503responses with bounded backoff. - Use an idempotency key when retrying audit creation.
- Resolve a
409based on the deployment state rather than retrying indefinitely.
Data ownership and retention
Audit and deployment resources are scoped to the authenticated customer. Requests for another customer’s resources do not expose those resources.
Runtime audit records currently expire after 90 days. Download or retain JSON and JUnit reports in the CI/CD system when longer retention is required.
Run an audit