openapi: 3.1.0
info:
  title: Guardian Logs API
  version: "1.0.0"
  description: >
    The Guardian Logs HTTP API.


    There are TWO strictly separate credential systems, and neither can act as
    the other:


    * **Ingest token** (`gl_live_<publicId>_<secret>`) — write-only. Used ONLY by
      `POST /api/v1/ingest`. Permanently bound to one organization + app + source;
      the destination is derived from the credential, never from the request body.
      Cannot read any resource.
    * **General API key** (`gl_api_<publicId>_<secret>`) — organization-scoped,
      read-only. Used by every `GET /api/v1/*` resource endpoint, gated by
      explicit read scopes. Cannot ingest.


    Both are presented as `Authorization: Bearer <credential>`. Presenting the
    wrong credential type for an endpoint yields `401` (the prefixes are validated
    per endpoint). Authentication failures are uniform — the API never reveals
    whether a token, key, org, app, or resource exists.


    Custom read scopes (`apps:read`, `issues:read`, …) cannot be expressed on an
    HTTP bearer security scheme in OpenAPI, so each protected operation documents
    its requirement with the `x-required-scope` extension AND in its description.
  contact:
    name: Guardian Logs Developer Portal
    url: https://dev.guardianlogs.com
  license:
    name: Proprietary

servers:
  - url: https://guardianlogs.com
    description: Production

tags:
  - name: Health
    description: Public, unauthenticated service health.
  - name: Ingest
    description: Write log events (ingest token only).
  - name: Apps
    description: Read monitored applications and their log sources.
  - name: Issues
    description: Read grouped issues.
  - name: Signals
    description: Read individual detected events.
  - name: Deployments
    description: Read deployment records.

paths:
  /api/health:
    get:
      tags: [Health]
      operationId: getHealth
      summary: Public service health (liveness + DB).
      description: >
        Unauthenticated liveness/readiness probe. Returns coarse status only —
        never secrets, connection strings, environment, or stack traces.
        Returns `200` when the database is reachable, `503` when it is not.
      security: []
      responses:
        "200":
          description: Service healthy (database reachable).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
              example:
                ok: true
                db: true
        "503":
          description: Service unhealthy (database unreachable).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"
              example:
                ok: false
                db: false

  /api/v1/ingest:
    post:
      tags: [Ingest]
      operationId: ingestEvents
      summary: Ingest one event or a batch of events.
      description: >
        Ingest a single event object OR a batch envelope `{ "events": [ … ] }`
        (at most 500 events). The organization, app, and source are derived
        entirely from the ingest token — any org/app/source fields in the body
        are ignored.


        Batches are partial-success: each event is validated independently and
        reported in `results[]`. A malformed individual event is `rejected`
        without failing the batch; only a body that is not valid JSON, or is
        neither an object nor an array of events, fails the whole request with
        `400`. Deduplication is on `(source, idempotencyKey)`; a repeated key is
        a no-op counted under `duplicates`.


        Requires an **ingest token** (`gl_live_…`). A general API key (`gl_api_…`)
        is rejected with `401`.
      security:
        - ingestToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/IngestEvent"
                - $ref: "#/components/schemas/IngestBatch"
            examples:
              singleEvent:
                summary: A single event
                value:
                  timestamp: "2026-09-13T16:22:11Z"
                  level: error
                  message: Failed to process document
                  errorClass: TypeError
                  environment: production
                  service: web
                  host: app01
                  deployment: c8381ab
                  route: /api/process
                  metadata:
                    requestId: abc123
              batch:
                summary: A batch of events with idempotency keys
                value:
                  events:
                    - timestamp: "2026-09-13T16:22:11Z"
                      level: error
                      message: boom
                      idempotencyKey: evt-1
                    - timestamp: "2026-09-13T16:22:12Z"
                      level: warn
                      message: slow
                      idempotencyKey: evt-2
      responses:
        "202":
          description: >
            Accepted. The batch was received; inspect `results[]` for each
            event's outcome (`accepted`, `duplicate`, or `rejected`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestResult"
              examples:
                allAccepted:
                  value:
                    ok: true
                    batchId: clw9k2xm0000abcd1234efgh
                    received: 1
                    accepted: 1
                    duplicates: 0
                    rejected: 0
                    results:
                      - index: 0
                        status: accepted
                partialSuccess:
                  value:
                    ok: true
                    batchId: clw9k2xm0000abcd1234efgh
                    received: 2
                    accepted: 1
                    duplicates: 0
                    rejected: 1
                    results:
                      - index: 0
                        status: accepted
                      - index: 1
                        status: rejected
                        error: "String must contain at least 1 character(s)"
        "400":
          description: >
            Malformed request body — not valid JSON (`malformed_json`) or an empty
            event set (`empty`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: malformed_json
                  message: Request body is not valid JSON.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: The credential's log source is disabled (`source_disabled`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: source_disabled
                  message: This log source is disabled.
        "413":
          description: >
            Payload too large — body over 1 MB (`payload_too_large`) or more than
            500 events (`batch_too_large`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: batch_too_large
                  message: At most 500 events per request.
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/apps:
    get:
      tags: [Apps]
      operationId: listApps
      summary: List applications in your organization.
      description: |
        List monitored applications belonging to the API key's organization.

        **Required scope:** `apps:read` (`APPS_READ`).
      x-required-scope: apps:read
      security:
        - apiKey: []
      parameters:
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of applications.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/App"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
              example:
                data:
                  - id: clapp0001kidscuts000000
                    key: kids-cuts
                    name: Kids Cuts
                    env: production
                    purpose: Booking and POS for the salon chain.
                    createdAt: "2026-08-01T12:00:00.000Z"
                pagination:
                  nextCursor: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/apps/{id}:
    get:
      tags: [Apps]
      operationId: getApp
      summary: Get one application by id.
      description: |
        Fetch a single application in the key's organization. A cross-tenant or
        nonexistent id both return `404` (indistinguishable).

        **Required scope:** `apps:read` (`APPS_READ`).
      x-required-scope: apps:read
      security:
        - apiKey: []
      parameters:
        - $ref: "#/components/parameters/AppIdPath"
      responses:
        "200":
          description: The application.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/AppDetail"
              example:
                data:
                  id: clapp0001kidscuts000000
                  key: kids-cuts
                  name: Kids Cuts
                  env: production
                  purpose: Booking and POS for the salon chain.
                  ownerContact: ops@kidscuts.example
                  businessImpactNotes: Revenue-critical during business hours.
                  createdAt: "2026-08-01T12:00:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/apps/{id}/sources:
    get:
      tags: [Apps]
      operationId: listAppSources
      summary: List an application's log sources.
      description: |
        List the log sources for an application in the key's organization. If the
        application does not belong to the key's org (or does not exist), returns
        `404`.

        **Required scope:** `sources:read` (`SOURCES_READ`).
      x-required-scope: sources:read
      security:
        - apiKey: []
      parameters:
        - $ref: "#/components/parameters/AppIdPath"
      responses:
        "200":
          description: The application's log sources (not paginated).
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Source"
              example:
                data:
                  - id: clsrc0001errors0000000
                    type: APP_ERROR
                    transport: PUSH_HTTPS
                    enabled: true
                    redactionProfile: default
                    createdAt: "2026-08-01T12:05:00.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/issues:
    get:
      tags: [Issues]
      operationId: listIssues
      summary: List issues in your organization.
      description: |
        List grouped issues in the key's organization, newest first.

        **Required scope:** `issues:read` (`ISSUES_READ`).
      x-required-scope: issues:read
      security:
        - apiKey: []
      parameters:
        - name: status
          in: query
          required: false
          description: Filter by issue status.
          schema:
            $ref: "#/components/schemas/IssueStatus"
        - name: severity
          in: query
          required: false
          description: Filter by severity.
          schema:
            $ref: "#/components/schemas/Severity"
        - name: appId
          in: query
          required: false
          description: Filter by application id.
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of issues.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Issue"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
              example:
                data:
                  - id: clissue001abcd00000000
                    title: "TypeError: cannot read properties of undefined"
                    status: OPEN
                    severity: HIGH
                    appId: clapp0001kidscuts000000
                    fingerprint: 7f3a9c2e1b
                    eventCount: 42
                    isRepeatOffender: true
                    assignedToUserId: null
                    firstSeenAt: "2026-09-10T08:14:00.000Z"
                    lastSeenAt: "2026-09-13T16:22:11.000Z"
                pagination:
                  nextCursor: Y2xpc3N1ZTAwMWFiY2QwMDAwMDAwMA
        "400":
          description: >
            Invalid `status` or `severity` filter value (`invalid_request`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                error:
                  code: invalid_request
                  message: "Invalid status. One of: OPEN, INVESTIGATING, CAUSE_CONFIRMED, CAUSE_RULED_OUT, RESOLVED, IGNORED."
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/issues/{id}:
    get:
      tags: [Issues]
      operationId: getIssue
      summary: Get one issue by id.
      description: |
        Fetch a single issue in the key's organization. Cross-tenant or
        nonexistent id both return `404`. The detail response adds `createdAt`
        and `updatedAt` over the list shape.

        **Required scope:** `issues:read` (`ISSUES_READ`).
      x-required-scope: issues:read
      security:
        - apiKey: []
      parameters:
        - name: id
          in: path
          required: true
          description: Issue id.
          schema:
            type: string
      responses:
        "200":
          description: The issue.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/IssueDetail"
              example:
                data:
                  id: clissue001abcd00000000
                  title: "TypeError: cannot read properties of undefined"
                  status: OPEN
                  severity: HIGH
                  appId: clapp0001kidscuts000000
                  fingerprint: 7f3a9c2e1b
                  eventCount: 42
                  isRepeatOffender: true
                  assignedToUserId: null
                  firstSeenAt: "2026-09-10T08:14:00.000Z"
                  lastSeenAt: "2026-09-13T16:22:11.000Z"
                  createdAt: "2026-09-10T08:14:01.000Z"
                  updatedAt: "2026-09-13T16:22:12.000Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/signals:
    get:
      tags: [Signals]
      operationId: listSignals
      summary: List signals (individual detected events).
      description: |
        List the individual detections (events) that get grouped into issues, in
        the key's organization, newest first.

        **Required scope:** `signals:read` (`SIGNALS_READ`).
      x-required-scope: signals:read
      security:
        - apiKey: []
      parameters:
        - name: appId
          in: query
          required: false
          description: Filter by application id.
          schema:
            type: string
        - name: issueId
          in: query
          required: false
          description: Filter by the issue a signal was grouped into.
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of signals.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Signal"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
              example:
                data:
                  - id: clsig0001abcd000000000
                    occurredAt: "2026-09-13T16:22:11.000Z"
                    level: ERROR
                    message: Failed to process document
                    route: /api/process
                    host: app01
                    appId: clapp0001kidscuts000000
                    fingerprint: 7f3a9c2e1b
                    issueId: clissue001abcd00000000
                pagination:
                  nextCursor: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /api/v1/deployments:
    get:
      tags: [Deployments]
      operationId: listDeployments
      summary: List deployments in your organization.
      description: |
        List deployment records for the key's organization, newest first.
        Deployments are scoped via their parent application's organization.

        **Required scope:** `deployments:read` (`DEPLOYMENTS_READ`).
      x-required-scope: deployments:read
      security:
        - apiKey: []
      parameters:
        - name: appId
          in: query
          required: false
          description: Filter by application id.
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: A page of deployments.
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Deployment"
                  pagination:
                    $ref: "#/components/schemas/Pagination"
              example:
                data:
                  - id: cldep0001abcd000000000
                    appId: clapp0001kidscuts000000
                    deployedAt: "2026-09-13T15:40:00.000Z"
                    version: c8381ab
                    actor: ci-bot
                    source: github
                    notes: null
                pagination:
                  nextCursor: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

components:
  securitySchemes:
    ingestToken:
      type: http
      scheme: bearer
      description: >
        Ingest token `gl_live_<publicId>_<secret>`, presented as
        `Authorization: Bearer gl_live_…`. Write-only; usable only on
        `POST /api/v1/ingest`. A general API key (`gl_api_…`) is rejected here.
    apiKey:
      type: http
      scheme: bearer
      description: >
        General API key `gl_api_<publicId>_<secret>`, presented as
        `Authorization: Bearer gl_api_…`. Read-only; each endpoint additionally
        requires the scope named in its `x-required-scope`. An ingest token
        (`gl_live_…`) is rejected here.

  parameters:
    Limit:
      name: limit
      in: query
      required: false
      description: Maximum items to return (1–100). Values outside the range are clamped; default 50.
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 50
    Cursor:
      name: cursor
      in: query
      required: false
      description: Opaque pagination cursor from a previous response's `pagination.nextCursor`.
      schema:
        type: string
    AppIdPath:
      name: id
      in: path
      required: true
      description: Application id.
      schema:
        type: string

  responses:
    Unauthorized:
      description: >
        Missing, malformed, unknown, revoked, or expired credential — or the wrong
        credential type for this endpoint. Uniform; the reason is never disclosed.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: unauthorized
              message: Invalid or expired API key.
    Forbidden:
      description: The credential is valid but is missing the required read scope.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: forbidden
              message: "This API key is missing the required scope: issues:read."
    NotFound:
      description: >
        Resource not found. A cross-tenant id is indistinguishable from a
        nonexistent one.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            error:
              code: not_found
              message: Application not found.
    RateLimited:
      description: >
        Rate limit exceeded. `scope` is `token` (per-credential) or `org`
        (per-organization); wait `retryAfterSec` seconds before retrying.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/RateLimitError"
          example:
            error:
              code: rate_limited
              message: Rate limit exceeded.
              retryAfterSec: 12
              scope: token

  schemas:
    HealthStatus:
      type: object
      required: [ok, db]
      properties:
        ok:
          type: boolean
          description: True when the service is healthy (mirrors `db`).
        db:
          type: boolean
          description: True when the database is reachable.

    IngestEvent:
      type: object
      required: [message]
      description: A single log event. Client identity (org/app/source) is never read from the body.
      properties:
        message:
          type: string
          minLength: 1
          maxLength: 8000
          description: The log line / error message.
        timestamp:
          type: string
          format: date-time
          description: ISO-8601. Defaults to receipt time. An invalid timestamp rejects the individual event.
        level:
          type: string
          maxLength: 20
          description: >
            `error`/`warn`/`info`. Aliases map sensibly (`fatal`/`err`/`critical` → ERROR,
            `warning` → WARN, `debug`/`trace`/`notice`/`log` → INFO). Defaults to `error`.
        errorClass:
          type: string
          maxLength: 200
          description: e.g. `TypeError`. Improves grouping.
        environment:
          type: string
          maxLength: 60
        service:
          type: string
          maxLength: 120
        host:
          type: string
          maxLength: 200
        deployment:
          type: string
          maxLength: 120
          description: Build/deploy id (e.g. a git SHA). Aids deploy correlation.
        route:
          type: string
          maxLength: 500
          description: Promoted for grouping. Also read from `metadata.route`.
        eventId:
          type: string
          minLength: 1
          maxLength: 200
          description: Alternative idempotency key (used if `idempotencyKey` is absent).
        idempotencyKey:
          type: string
          minLength: 1
          maxLength: 200
          description: Stable dedupe key. Dedupe is on `(source, idempotencyKey)`.
        metadata:
          type: object
          additionalProperties: true
          description: Free-form structured context. Serialized form must be ≤ 8 KB.

    IngestBatch:
      type: object
      required: [events]
      description: A batch envelope of up to 500 events.
      properties:
        events:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: "#/components/schemas/IngestEvent"

    IngestItemResult:
      type: object
      required: [index, status]
      properties:
        index:
          type: integer
          description: Zero-based index of the event in the submitted request.
        status:
          type: string
          enum: [accepted, duplicate, rejected]
        error:
          type: string
          description: Present only when `status` is `rejected`; the per-event validation error.

    IngestResult:
      type: object
      required: [ok, batchId, received, accepted, duplicates, rejected, results]
      properties:
        ok:
          type: boolean
        batchId:
          type: [string, "null"]
          description: Id of the stored batch, or null when no events were persisted.
        received:
          type: integer
          description: Number of events in the request.
        accepted:
          type: integer
        duplicates:
          type: integer
        rejected:
          type: integer
        results:
          type: array
          items:
            $ref: "#/components/schemas/IngestItemResult"

    App:
      type: object
      description: Monitored application (list shape).
      required: [id, key, name, env, purpose, createdAt]
      properties:
        id:
          type: string
        key:
          type: string
          description: Stable slug, unique within the organization (e.g. `kids-cuts`).
        name:
          type: string
        env:
          type: string
          description: Environment label (e.g. `production`).
        purpose:
          type: string
        createdAt:
          type: string
          format: date-time

    AppDetail:
      type: object
      description: Monitored application (detail shape — adds owner/impact fields).
      required: [id, key, name, env, purpose, ownerContact, businessImpactNotes, createdAt]
      properties:
        id:
          type: string
        key:
          type: string
        name:
          type: string
        env:
          type: string
        purpose:
          type: string
        ownerContact:
          type: [string, "null"]
        businessImpactNotes:
          type: [string, "null"]
        createdAt:
          type: string
          format: date-time

    Source:
      type: object
      description: A log source belonging to an application.
      required: [id, type, transport, enabled, redactionProfile, createdAt]
      properties:
        id:
          type: string
        type:
          type: string
          enum: [APP_ERROR, ACTIVITY, EMAIL, NGINX, PM2, OS, DB_SLOWQUERY, HEALTH]
        transport:
          type: string
          enum: [PUSH_HTTPS, OBJECT_STORE]
        enabled:
          type: boolean
        redactionProfile:
          type: [string, "null"]
          description: Named redaction profile applied at the source.
        createdAt:
          type: string
          format: date-time

    IssueStatus:
      type: string
      enum: [OPEN, INVESTIGATING, CAUSE_CONFIRMED, CAUSE_RULED_OUT, RESOLVED, IGNORED]

    Severity:
      type: string
      enum: [CRITICAL, HIGH, MEDIUM, LOW]

    Issue:
      type: object
      description: A grouped issue (list shape).
      required:
        [id, title, status, severity, appId, fingerprint, eventCount, isRepeatOffender, assignedToUserId, firstSeenAt, lastSeenAt]
      properties:
        id:
          type: string
        title:
          type: string
        status:
          $ref: "#/components/schemas/IssueStatus"
        severity:
          $ref: "#/components/schemas/Severity"
        appId:
          type: string
        fingerprint:
          type: string
          description: Grouping fingerprint, unique within the organization.
        eventCount:
          type: integer
        isRepeatOffender:
          type: boolean
        assignedToUserId:
          type: [string, "null"]
        firstSeenAt:
          type: string
          format: date-time
        lastSeenAt:
          type: string
          format: date-time

    IssueDetail:
      allOf:
        - $ref: "#/components/schemas/Issue"
        - type: object
          required: [createdAt, updatedAt]
          properties:
            createdAt:
              type: string
              format: date-time
            updatedAt:
              type: string
              format: date-time

    Signal:
      type: object
      description: An individual detected event (signal).
      required: [id, occurredAt, level, message, route, host, appId, fingerprint, issueId]
      properties:
        id:
          type: string
        occurredAt:
          type: string
          format: date-time
        level:
          type: string
          enum: [ERROR, WARN, INFO]
        message:
          type: string
        route:
          type: [string, "null"]
        host:
          type: [string, "null"]
        appId:
          type: string
        fingerprint:
          type: string
        issueId:
          type: [string, "null"]
          description: The issue this signal was grouped into, if any.

    Deployment:
      type: object
      description: A deployment record for an application.
      required: [id, appId, deployedAt, version, actor, source, notes]
      properties:
        id:
          type: string
        appId:
          type: string
        deployedAt:
          type: string
          format: date-time
        version:
          type: [string, "null"]
          description: Build/version identifier (e.g. a git SHA).
        actor:
          type: [string, "null"]
        source:
          type: string
          description: How the record was created (e.g. `manual`, `github`).
        notes:
          type: [string, "null"]

    Pagination:
      type: object
      required: [nextCursor]
      properties:
        nextCursor:
          type: [string, "null"]
          description: Opaque cursor for the next page, or null on the last page.

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
            message:
              type: string
              description: Human-readable explanation.

    RateLimitError:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, retryAfterSec, scope]
          properties:
            code:
              type: string
              enum: [rate_limited]
            message:
              type: string
            retryAfterSec:
              type: integer
              description: Seconds to wait before retrying.
            scope:
              type: string
              enum: [token, org]
              description: Which budget was exceeded (per-credential or per-organization).
