openapi: 3.1.0
info:
  title: PWA Today Runtime Audit API
  version: v1
  license:
    name: Proprietary
  description: |
    Customer API for creating and retrieving asynchronous PWA Today runtime audits.
    Audit execution status and quality-gate outcome are separate: a completed audit
    can have a failed quality gate.
servers:
  - url: https://api.pwa.today
paths:
  /token:
    post:
      operationId: createAccessToken
      summary: Exchange customer client credentials for an access token
      security:
        - customerClientCredentials: []
      responses:
        '200':
          description: Access token created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessToken'
        '400':
          $ref: '#/components/responses/InvalidClient'
        '401':
          description: HTTP Basic client credentials are required.
          headers:
            WWW-Authenticate:
              schema:
                type: string
              example: Basic realm="PWA Today token"
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          $ref: '#/components/responses/AuthorizationServiceUnavailable'
  /v1/audits:
    post:
      operationId: createAudit
      summary: Create an asynchronous runtime audit
      description: |
        Starts an audit for a verified application owned by the authenticated customer.
        Use one Idempotency-Key for one logical audit run. Reusing it returns the
        original audit instead of creating a replacement.
      security:
        - customerAccessToken:
            - https://api.pwa.today/checks.run
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuditRequest'
            example:
              url: https://example.com
              applicationId: example.com
              profile: standard
              include: []
              exclude: []
              options: {}
              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
      responses:
        '202':
          description: Audit accepted, or an existing audit was reused for the idempotency key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditCreated'
              example:
                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
        '400':
          $ref: '#/components/responses/InvalidRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
  /v1/audits/{auditId}:
    get:
      operationId: getAudit
      summary: Get audit execution status and quality-gate state
      description: |
        Audits are scoped to the authenticated customer. An audit owned by another
        customer is returned as not found. queued and running audits can have null
        score and timestamps, and their qualityGate does not yet contain passed.
      security:
        - customerAccessToken:
            - https://api.pwa.today/checks.run
      parameters:
        - $ref: '#/components/parameters/AuditId'
      responses:
        '200':
          description: Audit status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Audit'
              example:
                auditId: 36a14af6-e488-475d-8c4e-ae262e5bff99
                applicationId: example.com
                applicationName: example.com
                url: https://example.com/
                status: completed
                profile: full
                selectedChecks: [manifest, offline, offline-navigation]
                allowedOrigins: []
                score: 100
                qualityGate:
                  minimumScore: 90
                  failOn: [critical, high]
                  failOnWarnings: false
                  passed: true
                  failedSeverities: []
                  warningCount: 0
                  errorCount: 0
                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
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AuditNotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/audits/{auditId}/results:
    get:
      operationId: getAuditResults
      summary: Get persisted ordered audit check results
      description: Results may be partial while terminal is false.
      security:
        - customerAccessToken:
            - https://api.pwa.today/checks.run
      parameters:
        - $ref: '#/components/parameters/AuditId'
      responses:
        '200':
          description: Ordered persisted check results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditResults'
              example:
                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
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/AuditNotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /v1/applications/{applicationId}/audits:
    get:
      operationId: listApplicationAudits
      summary: List audits for an application
      security:
        - customerAccessToken:
            - https://api.pwa.today/checks.run
      parameters:
        - $ref: '#/components/parameters/ApplicationId'
        - name: limit
          in: query
          description: Defaults to 20. Values are limited to 1 through 100.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Audits in newest-first order.
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [applicationId, audits]
                properties:
                  applicationId:
                    $ref: '#/components/schemas/ApplicationId'
                  audits:
                    type: array
                    items:
                      $ref: '#/components/schemas/Audit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  securitySchemes:
    customerClientCredentials:
      type: http
      scheme: basic
      description: Customer client ID and client secret.
    customerAccessToken:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://api.pwa.today/token
          scopes:
            https://api.pwa.today/checks.run: Run and retrieve customer runtime audits.
  parameters:
    AuditId:
      name: auditId
      in: path
      required: true
      schema:
        type: string
      description: Customer-scoped audit identifier.
    ApplicationId:
      name: applicationId
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/ApplicationId'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: A customer-scoped key for one logical audit run.
      schema:
        type: string
        maxLength: 200
  schemas:
    AccessToken:
      type: object
      additionalProperties: false
      required: [access_token, expires_in, token_type]
      properties:
        access_token:
          type: string
        expires_in:
          type: number
          description: Token lifetime in seconds.
          example: 3600
        token_type:
          type: string
          example: Bearer
        scope:
          type: string
    AuditRequest:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          maxLength: 2048
          description: Public HTTPS URL without credentials or a custom port. Fragments are removed.
        applicationId:
          $ref: '#/components/schemas/ApplicationId'
        authentication:
          $ref: '#/components/schemas/AuditAuthentication'
        allowedOrigins:
          type: array
          maxItems: 10
          items:
            type: string
            format: uri
          description: Additional public HTTPS origins without paths or query strings.
        profile:
          type: string
          enum: [quick, standard, full, custom]
          default: standard
        include:
          type: array
          default: []
          items:
            $ref: '#/components/schemas/CheckId'
        exclude:
          type: array
          default: []
          items:
            $ref: '#/components/schemas/CheckId'
        disabledRules:
          type: object
          additionalProperties:
            type: array
            items:
              type: string
          default: {}
          description: Supported disabled rule codes for selected checks.
        options:
          type: object
          additionalProperties:
            type: object
          default: {}
          description: Per-check options keyed by stable check ID.
        qualityGate:
          $ref: '#/components/schemas/QualityGateRequest'
        source:
          $ref: '#/components/schemas/SourceMetadata'
    AuditAuthentication:
      type: object
      description: Application authentication supplied for a single audit. Do not place it in logs or artifacts.
      properties:
        authorization:
          type: string
          maxLength: 2048
          pattern: '^Bearer\\s+\\S+$'
        headers:
          type: object
          properties:
            authorization:
              type: string
              maxLength: 2048
              pattern: '^Bearer\\s+\\S+$'
        cookies:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: object
            required: [name, value]
            properties:
              name:
                type: string
              value:
                type: string
                minLength: 1
                maxLength: 2048
        sessionStorage:
          type: object
          minProperties: 1
          maxProperties: 10
          additionalProperties:
            type: string
            minLength: 1
            maxLength: 8192
    ApplicationId:
      type: string
      pattern: '^[a-zA-Z0-9.-]{1,253}$'
      description: Application hostname.
    CheckId:
      type: string
      enum:
        - manifest
        - offline
        - cache
        - offline-navigation
        - service-worker-first-installation
        - service-worker-handlers
        - service-worker-update
        - service-worker-deployment
        - before-install-prompt
        - offline-request-retry
        - push-notifications
    AuditStatus:
      type: string
      enum: [queued, running, completed, partially-completed, failed]
    CheckStatus:
      type: string
      enum: [passed, failed, warning, blocked, not-applicable, skipped, error]
    Severity:
      type: string
      enum: [critical, high, medium, low]
    QualityGateRequest:
      type: object
      properties:
        minimumScore:
          type: number
          minimum: 0
          maximum: 100
          default: 90
        failOn:
          type: array
          default: [critical, high]
          items:
            $ref: '#/components/schemas/Severity'
        failOnWarnings:
          type: boolean
          default: false
    QualityGate:
      allOf:
        - $ref: '#/components/schemas/QualityGateRequest'
        - type: object
          properties:
            passed:
              type: boolean
              description: Present after the audit reaches a terminal state.
            failedSeverities:
              type: array
              items:
                $ref: '#/components/schemas/Severity'
            warningCount:
              type: integer
              minimum: 0
            errorCount:
              type: integer
              minimum: 0
    SourceMetadata:
      type: object
      properties:
        type: { type: string, maxLength: 2048 }
        provider: { type: string, maxLength: 2048 }
        repository: { type: string, maxLength: 2048 }
        branch: { type: string, maxLength: 2048 }
        commit: { type: string, maxLength: 2048 }
        pullRequest: { type: string, maxLength: 2048 }
        pipelineUrl: { type: string, maxLength: 2048 }
        environment: { type: string, maxLength: 2048 }
        cliVersion: { type: string, maxLength: 2048 }
      additionalProperties: true
      description: Unsupported fields and non-string values are omitted by the API.
    Versions:
      type: object
      additionalProperties: true
      properties:
        cliVersion:
          type: [string, 'null']
        apiVersion:
          type: string
        engineVersion:
          type: string
        rulesetVersion:
          type: string
        engineRevision:
          type: [string, 'null']
    Audit:
      type: object
      additionalProperties: true
      properties:
        auditId:
          type: string
        applicationId:
          $ref: '#/components/schemas/ApplicationId'
        applicationName:
          type: string
        url:
          type: string
          format: uri
        status:
          $ref: '#/components/schemas/AuditStatus'
        profile:
          type: string
        selectedChecks:
          type: array
          items:
            $ref: '#/components/schemas/CheckId'
        allowedOrigins:
          type: array
          items:
            type: string
            format: uri
        score:
          type: [number, 'null']
        qualityGate:
          $ref: '#/components/schemas/QualityGate'
        source:
          $ref: '#/components/schemas/SourceMetadata'
        versions:
          $ref: '#/components/schemas/Versions'
        createdAt:
          type: string
          format: date-time
        startedAt:
          type: [string, 'null']
          format: date-time
        completedAt:
          type: [string, 'null']
          format: date-time
        error:
          type: [string, 'null']
    AuditCreated:
      type: object
      additionalProperties: false
      required: [auditId, status, reused, statusUrl, resultsUrl]
      properties:
        auditId:
          type: string
        status:
          $ref: '#/components/schemas/AuditStatus'
        reused:
          type: boolean
        statusUrl:
          type: string
        resultsUrl:
          type: string
    AuditResults:
      type: object
      additionalProperties: false
      required: [auditId, status, terminal, results]
      properties:
        auditId:
          type: string
        status:
          $ref: '#/components/schemas/AuditStatus'
        terminal:
          type: boolean
        results:
          type: array
          items:
            $ref: '#/components/schemas/CheckResult'
    CheckResult:
      type: object
      additionalProperties: true
      properties:
        check:
          $ref: '#/components/schemas/CheckId'
        status:
          $ref: '#/components/schemas/CheckStatus'
        severity:
          $ref: '#/components/schemas/Severity'
        message:
          type: string
        details:
          type: object
          additionalProperties: true
        recommendation:
          type: string
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
        durationMs:
          type: number
    Error:
      type: object
      additionalProperties: true
      properties:
        code:
          type: string
        error:
          type: string
        auditId:
          type: string
        upgradeUrl:
          type: string
          format: uri
        applicationsUrl:
          type: string
          format: uri
  responses:
    InvalidRequest:
      description: The public request input is invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InvalidClient:
      description: The customer client credentials were rejected by the authorization service.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: An access token is missing or invalid.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: The customer is not entitled to run this audit or does not own the verified application.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    AuditNotFound:
      description: The audit does not exist for this customer.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Conflict:
      description: The audit reservation conflicted with another request. Retry with the same idempotency key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Audit allowance, concurrency, or API rate limit reached.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    AuthorizationServiceUnavailable:
      description: The authorization service could not be reached.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServiceUnavailable:
      description: The runtime audit could not be started.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    InternalError:
      description: The request could not be completed.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
