PWA Today documentation

Runtime Audit API Reference

Create audits, retrieve results, list application history, and coordinate service worker deployments.

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:

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.

PlanAccepted auditAllowanceApplications
Complimentary trialStandard runtime audit3 lifetime1
DeveloperStandard runtime audit100 per billing period3
TeamFull-suite selection or complete release audit250 per billing period15
BusinessFull-suite selection or complete release audit500 per billing period50

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:

  1. Create an audit with POST /v1/audits.
  2. Poll GET /v1/audits/{auditId}.
  3. Stop polling when the status is terminal.
  4. 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:

  • type
  • provider
  • repository
  • branch
  • commit
  • pullRequest
  • pipelineUrl
  • environment
  • cliVersion

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:

  • passed
  • failed
  • warning
  • not-applicable
  • skipped
  • error

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 completed or failed state.
  • 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/"
}
CodeMeaning
ACCOUNT_REQUIREDNo canonical customer account is available.
SUBSCRIPTION_REQUIREDBilling is inactive, ended, or otherwise unavailable.
PAYMENT_PAST_DUEPayment is past due and the grace period has ended.
AUDIT_TYPE_NOT_ALLOWEDThe requested profile is not included in the plan.
RELEASE_DEPLOYMENT_REQUIREDA complete release audit requires deployment validation.
AUDIT_LIMIT_REACHEDThe audit allowance for the lifetime trial or billing period is exhausted.
APPLICATION_REQUIREDAdd the audited hostname as an Application before running an audit.
APPLICATION_NOT_VERIFIEDVerify the Application before running an audit.
APPLICATION_LIMIT_REACHEDThe plan’s Application allowance is exhausted.
AUDIT_RESERVATION_CONFLICTAccount 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, and 403 as configuration, authentication, or billing failures.
  • Treat 429 as an allowance limit and do not retry the same request automatically.
  • Retry transient 500, 502, and 503 responses with bounded backoff.
  • Use an idempotency key when retrying audit creation.
  • Resolve a 409 based 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.