openapi: 3.1.0
info:
  title: AppVisionOS IAP API
  version: 0.11.0
  description: Immutable purchase ingestion, access level, recovery, and control-plane contract.
servers:
  - url: /
paths:
  /livez:
    get:
      operationId: getLiveness
      responses:
        "200":
          description: Process is alive.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Probe"
  /readyz:
    get:
      operationId: getReadiness
      responses:
        "200":
          description: Process and PostgreSQL are ready.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Probe"
        "503":
          description: PostgreSQL is unavailable.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Probe"
  /healthz:
    get:
      operationId: getHealth
      description: Compatibility alias for readiness.
      responses:
        "200":
          description: Ready.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Probe"
        "503":
          description: Not ready.
  /metrics:
    get:
      operationId: getMetrics
      responses:
        "200":
          description: Prometheus exposition.
          content:
            text/plain:
              schema:
                type: string
  /ingest/apple/assn:
    post:
      operationId: ingestAppleNotification
      description: Verifies an App Store Server Notifications V2 outer and nested JWS values, attributes its app, and atomically appends and projects or rejects it.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [signedPayload]
              properties:
                signedPayload:
                  type: string
              additionalProperties: false
      responses:
        "200":
          description: Delivery appended, deduplicated, or durably rejected.
        "503":
          description: PostgreSQL or Apple's OCSP responder is temporarily unavailable.
          headers:
            Retry-After:
              schema:
                type: string
  /ingest/google/rtdn:
    post:
      operationId: ingestGoogleNotification
      security:
        - googlePubSubOIDC: []
      description: Verifies Pub/Sub OIDC identity, appends the RTDN doorbell, fetches authoritative subscriptionsv2 or productsv2 state when required, and atomically projects all line items.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PubSubPush"
      responses:
        "200":
          description: Delivery appended, deduplicated, or durably rejected.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: The valid Google identity is not the configured Pub/Sub service account.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/receipts:
    post:
      operationId: submitReceipt
      security:
        - bearerAPIKey: []
      description: A secret server key may submit directly. A publishable client key must also present X-IAP-Customer-Token and app_user_id must match its authenticated subject. The service then verifies an Apple StoreKit transaction JWS or authoritatively fetches a Google purchase token and atomically reduces mapped access levels.
      parameters:
        - $ref: "#/components/parameters/CustomerToken"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReceiptSubmission"
      responses:
        "200":
          description: Receipt observation appended or deduplicated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestResult"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/TooLarge"
        "429":
          $ref: "#/components/responses/RateLimited"
        "422":
          $ref: "#/components/responses/InvalidRequest"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/subscribers/{app_user_id}:
    get:
      operationId: getSubscriber
      security:
        - bearerAPIKey: []
      description: Returns the current Apple and Google access level union for one app-scoped customer alias.
      parameters:
        - name: app_user_id
          in: path
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 512
        - $ref: "#/components/parameters/CustomerToken"
      responses:
        "200":
          description: Current subscriber access level projection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscriber"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No customer alias exists for this app-user id.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/product-sets/current:
    get:
      summary: Read the current product set
      operationId: getCurrentProductSet
      security:
        - bearerAPIKey: []
      description: Returns the authenticated app's current cross-store product presentation. Publishable keys also require a customer token; secret keys do not.
      parameters:
        - $ref: "#/components/parameters/CustomerToken"
      responses:
        "200":
          description: Current product set, or an explicit null when the app has not selected one.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CurrentProductSet"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/webhook-endpoints:
    get:
      operationId: listWebhookEndpoints
      security:
        - bearerAPIKey: []
      responses:
        "200":
          description: Non-deleted endpoints for the authenticated app.
          content:
            application/json:
              schema:
                type: object
                required: [endpoints]
                properties:
                  endpoints:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookEndpoint"
                additionalProperties: false
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Unavailable"
    post:
      operationId: createWebhookEndpoint
      security:
        - bearerAPIKey: []
      description: Creates an HTTPS endpoint and returns its Standard Webhooks signing secret once. Destinations that are literal private or special-purpose addresses are rejected; resolved addresses are checked again on every dial.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWebhookEndpoint"
      responses:
        "201":
          description: Endpoint and one-time signing secret. The response must not be cached.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedWebhookEndpoint"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/webhook-endpoints/{endpoint_id}:
    parameters:
      - $ref: "#/components/parameters/EndpointID"
    get:
      operationId: getWebhookEndpoint
      security:
        - bearerAPIKey: []
      responses:
        "200":
          description: Endpoint configuration without signing material.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
    patch:
      operationId: updateWebhookEndpoint
      security:
        - bearerAPIKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWebhookEndpoint"
      responses:
        "200":
          description: Updated endpoint.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookEndpoint"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
    delete:
      operationId: deleteWebhookEndpoint
      security:
        - bearerAPIKey: []
      description: Soft-deletes the endpoint, cancels queued work, and retains delivery history.
      responses:
        "204":
          description: Endpoint deleted.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/webhook-endpoints/{endpoint_id}/rotate-secret:
    parameters:
      - $ref: "#/components/parameters/EndpointID"
    post:
      operationId: rotateWebhookEndpointSecret
      security:
        - bearerAPIKey: []
      description: Returns a new signing secret once and dual-signs with the immediately previous secret for 24 hours.
      responses:
        "200":
          description: One-time rotated secret and overlap boundary.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RotatedWebhookSecret"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/webhook-deliveries:
    get:
      operationId: listWebhookDeliveries
      security:
        - bearerAPIKey: []
      parameters:
        - name: endpoint_id
          in: query
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/WebhookDeliveryStatus"
        - name: before_event_number
          in: query
          description: Exclusive keyset cursor over the monotonic, non-gapless ledger event number.
          schema:
            type: integer
            format: int64
            minimum: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: Newest-first delivery history.
          content:
            application/json:
              schema:
                type: object
                required: [deliveries]
                properties:
                  deliveries:
                    type: array
                    items:
                      $ref: "#/components/schemas/WebhookDelivery"
                additionalProperties: false
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/webhook-deliveries/{message_id}:
    parameters:
      - $ref: "#/components/parameters/MessageID"
    get:
      operationId: getWebhookDelivery
      security:
        - bearerAPIKey: []
      responses:
        "200":
          description: Delivery and every recorded attempt, including expired worker leases.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDeliveryDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/webhook-deliveries/{message_id}/retry:
    parameters:
      - $ref: "#/components/parameters/MessageID"
    post:
      operationId: retryWebhookDelivery
      security:
        - bearerAPIKey: []
      description: Starts a fresh 72-hour retry cycle for a terminal delivery while retaining its message id and prior attempt history. The endpoint must first be enabled.
      responses:
        "200":
          description: Requeued delivery.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebhookDelivery"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          description: Endpoint is disabled or delivery is already queued/in flight.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          $ref: "#/components/responses/Unavailable"
  /admin/v1/apps:
    get:
      operationId: listApps
      security:
        - adminBearer: []
      responses:
        "200":
          description: All active and disabled apps in stable creation order.
          content:
            application/json:
              schema:
                type: object
                required: [apps]
                properties:
                  apps:
                    type: array
                    items:
                      $ref: "#/components/schemas/App"
                additionalProperties: false
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Unavailable"
    post:
      operationId: createApp
      security:
        - adminBearer: []
      description: Creates an Apple, Google, or cross-platform app and returns its first API key exactly once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApp"
      responses:
        "201":
          description: App, safe key metadata, and one-time plaintext secret.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedApp"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
  /admin/v1/apps/{app_id}:
    parameters:
      - $ref: "#/components/parameters/AppID"
    get:
      operationId: getApp
      security:
        - adminBearer: []
      responses:
        "200":
          description: App configuration.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/App"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
    patch:
      operationId: updateApp
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateApp"
      responses:
        "200":
          description: Updated app. Re-enabling does not un-revoke old keys.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/App"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
    delete:
      operationId: disableApp
      security:
        - adminBearer: []
      description: Soft-disables ingestion, revokes all app API keys, disables webhooks, and retains audit history.
      responses:
        "204":
          description: App disabled.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
  /admin/v1/apps/{app_id}/api-keys:
    parameters:
      - $ref: "#/components/parameters/AppID"
    get:
      operationId: listAppAPIKeys
      security:
        - adminBearer: []
      responses:
        "200":
          description: Safe API-key metadata; plaintext keys are never returned.
          content:
            application/json:
              schema:
                type: object
                required: [api_keys]
                properties:
                  api_keys:
                    type: array
                    items:
                      $ref: "#/components/schemas/APIKey"
                additionalProperties: false
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
    post:
      operationId: createAppAPIKey
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label:
                  type: string
                  maxLength: 512
                kind:
                  type: string
                  enum: [secret, publishable]
                  default: secret
              additionalProperties: false
      responses:
        "201":
          description: Safe metadata and one-time plaintext secret.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedAPIKey"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
  /admin/v1/apps/{app_id}/api-keys/{key_id}:
    parameters:
      - $ref: "#/components/parameters/AppID"
      - $ref: "#/components/parameters/APIKeyID"
    delete:
      operationId: revokeAppAPIKey
      security:
        - adminBearer: []
      responses:
        "204":
          description: Key revoked.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/access-levels:
    get:
      operationId: listAccessLevels
      security:
        - bearerAPIKey: []
      responses:
        "200":
          description: Active app-scoped access level catalog.
          content:
            application/json:
              schema:
                type: object
                required: [access_levels]
                properties:
                  access_levels:
                    type: array
                    items:
                      $ref: "#/components/schemas/CatalogAccessLevel"
                additionalProperties: false
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Unavailable"
    post:
      operationId: createAccessLevel
      security:
        - bearerAPIKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAccessLevel"
      responses:
        "201":
          description: Access level created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogAccessLevel"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/access-levels/{access_level_id}:
    parameters:
      - $ref: "#/components/parameters/AccessLevelID"
    get:
      operationId: getAccessLevel
      security:
        - bearerAPIKey: []
      responses:
        "200":
          description: Access level.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogAccessLevel"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
    patch:
      operationId: updateAccessLevel
      security:
        - bearerAPIKey: []
      description: Updates display_name; lookup_key is immutable.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [display_name]
              properties:
                display_name:
                  type: string
                  maxLength: 512
              additionalProperties: false
      responses:
        "200":
          description: Access level updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CatalogAccessLevel"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
    delete:
      operationId: archiveAccessLevel
      security:
        - bearerAPIKey: []
      description: Archives the lookup key and ends active product access mappings without deleting replay history.
      responses:
        "204":
          description: Access level archived.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/product-access-mappings:
    get:
      operationId: listProductAccessMappings
      security:
        - bearerAPIKey: []
      responses:
        "200":
          description: Active Apple and Google product access mappings.
          content:
            application/json:
              schema:
                type: object
                required: [product_mappings]
                properties:
                  product_mappings:
                    type: array
                    items:
                      $ref: "#/components/schemas/ProductAccessMapping"
                additionalProperties: false
        "401":
          $ref: "#/components/responses/Unauthorized"
        "503":
          $ref: "#/components/responses/Unavailable"
    post:
      operationId: createProductAccessMapping
      security:
        - bearerAPIKey: []
      description: Starts a mapping after a short, safe ledger high-water boundary.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProductAccessMapping"
      responses:
        "201":
          description: Active mapping version.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductAccessMapping"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/product-access-mappings/{mapping_id}:
    parameters:
      - $ref: "#/components/parameters/MappingID"
    patch:
      operationId: updateProductAccessMapping
      security:
        - bearerAPIKey: []
      description: Ends the active version and starts a new grant-mode version at one ledger boundary.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [grant_mode]
              properties:
                grant_mode:
                  $ref: "#/components/schemas/GrantMode"
              additionalProperties: false
      responses:
        "200":
          description: New active mapping version.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProductAccessMapping"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/Unavailable"
    delete:
      operationId: deleteProductAccessMapping
      security:
        - bearerAPIKey: []
      description: Ends the active version after the current safe ledger head.
      responses:
        "204":
          description: Mapping version ended.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/client-tokens:
    post:
      operationId: createClientToken
      security:
        - bearerAPIKey: []
      description: Requires a secret app key. Mints an opaque, short-lived token binding a publishable-key client to one app_user_id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateClientToken"
      responses:
        "201":
          description: One-time opaque customer token.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreatedClientToken"
        "400":
          $ref: "#/components/responses/InvalidRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "503":
          $ref: "#/components/responses/Unavailable"
  /v1/client-tokens/{token_id}:
    parameters:
      - $ref: "#/components/parameters/ClientTokenID"
    delete:
      operationId: revokeClientToken
      security:
        - bearerAPIKey: []
      description: Requires a secret app key and revokes one app-scoped customer token immediately.
      responses:
        "204":
          description: Client token revoked.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
components:
  securitySchemes:
    googlePubSubOIDC:
      type: http
      scheme: bearer
      bearerFormat: Google OIDC ID token
    bearerAPIKey:
      type: http
      scheme: bearer
      bearerFormat: iap_sk_ secret or iap_pk_ publishable app key
    adminBearer:
      type: http
      scheme: bearer
      bearerFormat: root admin API key
  schemas:
    Probe:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [ok, unavailable]
      additionalProperties: false
    PubSubPush:
      type: object
      required: [message]
      properties:
        message:
          type: object
          required: [data, messageId]
          properties:
            data:
              type: string
              contentEncoding: base64
            messageId:
              type: string
            publishTime:
              type: string
              format: date-time
            attributes:
              type: object
              additionalProperties:
                type: string
          additionalProperties: true
        subscription:
          type: string
      additionalProperties: true
    ReceiptSubmission:
      type: object
      required: [platform, app_user_id, store_payload]
      properties:
        platform:
          type: string
          enum: [apple, google]
        app_user_id:
          type: string
          minLength: 1
          maxLength: 512
        product_type:
          type: string
          enum: [subscription, one_time_product]
          description: Required for Google; omitted for Apple.
        store_payload:
          type: string
          description: Apple StoreKit transaction jwsRepresentation or Google purchase token.
      additionalProperties: false
      allOf:
        - if:
            properties:
              platform:
                const: google
          then:
            required: [product_type]
    IngestResult:
      type: object
      required: [event_id, deduplicated]
      properties:
        event_id:
          type: string
          pattern: "^[1-9][0-9]*$"
          maxLength: 19
          description: Positive decimal int64 ledger id (maximum 9223372036854775807) encoded as a string for lossless JavaScript and Dart interoperability.
        deduplicated:
          type: boolean
      additionalProperties: false
    Subscriber:
      type: object
      required: [app_user_id, customer_id, access_levels]
      properties:
        app_user_id:
          type: string
        customer_id:
          type: string
          format: uuid
        access_levels:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/SubscriberAccessLevel"
      additionalProperties: false
    SubscriberAccessLevel:
      type: object
      required: [active, platform, product_id, last_verified_at, event_id]
      properties:
        active:
          type: boolean
        expires_at:
          type: string
          format: date-time
        platform:
          type: string
          enum: [apple, google]
        product_id:
          type: string
        last_verified_at:
          type: string
          format: date-time
        event_id:
          type: string
          pattern: "^[1-9][0-9]*$"
          maxLength: 19
          description: Positive decimal int64 ledger id (maximum 9223372036854775807) encoded as a string for lossless JavaScript and Dart interoperability.
      additionalProperties: false
    CurrentProductSet:
      type: object
      required: [product_set]
      properties:
        product_set:
          anyOf:
            - $ref: "#/components/schemas/ProductSet"
            - type: "null"
      additionalProperties: false
    ProductSet:
      type: object
      required: [id, identifier, display_name, packages, updated_at]
      properties:
        id:
          type: string
          format: uuid
        identifier:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9._-]+$"
        display_name:
          type: string
          minLength: 1
          maxLength: 256
        packages:
          type: array
          description: Ordered by ascending position; identifiers and positions are unique within the set.
          items:
            $ref: "#/components/schemas/ProductSetPackage"
        updated_at:
          type: string
          format: date-time
      additionalProperties: false
    ProductSetPackage:
      type: object
      required: [id, identifier, package_type, platform, product_id, position]
      properties:
        id:
          type: string
          format: uuid
        identifier:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9._-]+$"
        package_type:
          type: string
          enum: [custom, lifetime, annual, six_month, three_month, two_month, monthly, weekly]
        platform:
          type: string
          enum: [apple, google]
        product_id:
          type: string
          minLength: 1
          maxLength: 512
        base_plan_id:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9._-]+$"
          description: Optional Google Play base-plan identifier. Present only for Google packages; preserve it when selecting a storefront offer.
        position:
          type: integer
          minimum: 0
          maximum: 1000
      additionalProperties: false
    WebhookEndpoint:
      type: object
      required: [id, url, description, event_types, enabled, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          format: uri
          maxLength: 2048
        description:
          type: string
          maxLength: 512
        event_types:
          type: array
          minItems: 1
          maxItems: 100
          uniqueItems: true
          items:
            type: string
        enabled:
          type: boolean
        disabled_reason:
          type: string
          maxLength: 512
        disabled_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      additionalProperties: false
    CreateWebhookEndpoint:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
          pattern: "^https://"
          maxLength: 2048
        description:
          type: string
          maxLength: 512
        event_types:
          type: array
          minItems: 1
          maxItems: 100
          uniqueItems: true
          default: [access.changed]
          items:
            type: string
      additionalProperties: false
    UpdateWebhookEndpoint:
      type: object
      minProperties: 1
      properties:
        url:
          type: string
          format: uri
          pattern: "^https://"
          maxLength: 2048
        description:
          type: string
          maxLength: 512
        event_types:
          type: array
          minItems: 1
          maxItems: 100
          uniqueItems: true
          items:
            type: string
        enabled:
          type: boolean
      additionalProperties: false
    CreatedWebhookEndpoint:
      type: object
      required: [endpoint, secret]
      properties:
        endpoint:
          $ref: "#/components/schemas/WebhookEndpoint"
        secret:
          type: string
          pattern: "^whsec_"
          writeOnly: true
      additionalProperties: false
    RotatedWebhookSecret:
      type: object
      required: [endpoint_id, secret, rotated_at, overlap_ends_at]
      properties:
        endpoint_id:
          type: string
          format: uuid
        secret:
          type: string
          pattern: "^whsec_"
          writeOnly: true
        rotated_at:
          type: string
          format: date-time
        overlap_ends_at:
          type: string
          format: date-time
      additionalProperties: false
    WebhookDeliveryStatus:
      type: string
      enum: [pending, in_flight, retry, succeeded, dead_letter, canceled]
    WebhookDelivery:
      type: object
      required:
        - message_id
        - endpoint_id
        - event_number
        - event_type
        - status
        - attempt_count
        - cycle_started_at
        - created_at
        - updated_at
      properties:
        message_id:
          type: string
          pattern: "^msg_[A-Za-z0-9_-]{20,64}$"
        endpoint_id:
          type: string
          format: uuid
        event_number:
          type: integer
          format: int64
        event_type:
          type: string
        status:
          $ref: "#/components/schemas/WebhookDeliveryStatus"
        attempt_count:
          type: integer
          minimum: 0
        next_attempt_at:
          type: string
          format: date-time
        first_attempt_at:
          type: string
          format: date-time
        last_attempt_at:
          type: string
          format: date-time
        succeeded_at:
          type: string
          format: date-time
        last_http_status:
          type: integer
          minimum: 100
          maximum: 599
        last_error:
          type: string
          maxLength: 2048
        last_response_body:
          type: string
          description: UTF-8-safe preview capped at 64 KiB.
        cycle_started_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      additionalProperties: false
    WebhookDeliveryAttempt:
      type: object
      required: [attempt_number, started_at]
      properties:
        attempt_number:
          type: integer
          minimum: 1
        started_at:
          type: string
          format: date-time
        finished_at:
          type: string
          format: date-time
        http_status:
          type: integer
          minimum: 100
          maximum: 599
        error:
          type: string
          maxLength: 2048
        response_body:
          type: string
        duration_ms:
          type: integer
          format: int64
          minimum: 0
        succeeded:
          type: boolean
      additionalProperties: false
    WebhookDeliveryDetail:
      type: object
      required: [delivery, attempts]
      properties:
        delivery:
          $ref: "#/components/schemas/WebhookDelivery"
        attempts:
          type: array
          items:
            $ref: "#/components/schemas/WebhookDeliveryAttempt"
      additionalProperties: false
    OutboundWebhookEvent:
      type: object
      description: Exact JSON body signed using Standard Webhooks headers.
      required: [id, type, api_version, timestamp, event_number, data]
      properties:
        id:
          type: string
          pattern: "^evt_[0-9]+$"
        type:
          type: string
          example: access.changed
        api_version:
          type: string
          const: "2026-08-28"
        timestamp:
          type: string
          format: date-time
        event_number:
          type: integer
          format: int64
          description: Monotonic but not gapless ledger identifier.
        data:
          type: object
          additionalProperties: true
      additionalProperties: false
    App:
      type: object
      required: [id, name, enabled, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          minLength: 1
          maxLength: 512
        apple_bundle_id:
          type: string
          minLength: 1
          maxLength: 512
        apple_app_apple_id:
          type: integer
          format: int64
          minimum: 1
        google_package_name:
          type: string
          minLength: 1
          maxLength: 512
        enabled:
          type: boolean
        disabled_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      additionalProperties: false
    CreateApp:
      type: object
      required: [name]
      anyOf:
        - required: [apple_bundle_id]
        - required: [google_package_name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 512
        apple_bundle_id:
          type: string
          minLength: 1
          maxLength: 512
        apple_app_apple_id:
          type: integer
          format: int64
          minimum: 1
          description: Required for production Apple notification attribution.
        google_package_name:
          type: string
          minLength: 1
          maxLength: 512
        initial_key_label:
          type: string
          maxLength: 512
          default: default
      additionalProperties: false
    UpdateApp:
      type: object
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 512
        apple_bundle_id:
          type: [string, "null"]
          minLength: 1
          maxLength: 512
        apple_app_apple_id:
          type: [integer, "null"]
          format: int64
          minimum: 1
        google_package_name:
          type: [string, "null"]
          minLength: 1
          maxLength: 512
        enabled:
          type: boolean
      additionalProperties: false
    APIKey:
      type: object
      required: [id, kind, label, hint, created_at]
      properties:
        id:
          type: string
          format: uuid
        kind:
          type: string
          enum: [secret, publishable]
        label:
          type: string
          maxLength: 512
        hint:
          type: string
          maxLength: 80
        created_at:
          type: string
          format: date-time
        revoked_at:
          type: string
          format: date-time
      additionalProperties: false
    CreatedAPIKey:
      type: object
      required: [api_key, secret]
      properties:
        api_key:
          $ref: "#/components/schemas/APIKey"
        secret:
          type: string
          pattern: "^iap_(sk|pk)_"
          writeOnly: true
      additionalProperties: false
    CreatedApp:
      type: object
      required: [app, api_key, secret]
      properties:
        app:
          $ref: "#/components/schemas/App"
        api_key:
          $ref: "#/components/schemas/APIKey"
        secret:
          type: string
          pattern: "^iap_sk_"
          writeOnly: true
      additionalProperties: false
    CatalogAccessLevel:
      type: object
      required: [id, lookup_key, display_name, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        lookup_key:
          type: string
          pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$"
        display_name:
          type: string
          maxLength: 512
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
      additionalProperties: false
    CreateAccessLevel:
      type: object
      required: [lookup_key]
      properties:
        lookup_key:
          type: string
          pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$"
        display_name:
          type: string
          maxLength: 512
      additionalProperties: false
    GrantMode:
      type: string
      enum: [store_managed, lifetime]
    ProductAccessMapping:
      type: object
      required:
        - id
        - platform
        - product_id
        - access_level_id
        - access_level_key
        - grant_mode
        - starts_after_event_id
        - created_at
      properties:
        id:
          type: string
          format: uuid
        platform:
          type: string
          enum: [apple, google]
        product_id:
          type: string
          minLength: 1
          maxLength: 512
        access_level_id:
          type: string
          format: uuid
        access_level_key:
          type: string
        grant_mode:
          $ref: "#/components/schemas/GrantMode"
        starts_after_event_id:
          type: integer
          format: int64
          minimum: 0
          description: Mapping applies to source events with a greater ledger id.
        created_at:
          type: string
          format: date-time
      additionalProperties: false
    CreateProductAccessMapping:
      type: object
      required: [platform, product_id, access_level_id]
      properties:
        platform:
          type: string
          enum: [apple, google]
        product_id:
          type: string
          minLength: 1
          maxLength: 512
        access_level_id:
          type: string
          format: uuid
        grant_mode:
          $ref: "#/components/schemas/GrantMode"
      additionalProperties: false
    CreateClientToken:
      type: object
      required: [app_user_id]
      properties:
        app_user_id:
          type: string
          minLength: 1
          maxLength: 512
        ttl_seconds:
          type: integer
          format: int64
          minimum: 300
          maximum: 86400
          default: 3600
      additionalProperties: false
    CreatedClientToken:
      type: object
      required: [id, app_user_id, hint, expires_at, token]
      properties:
        id:
          type: string
          format: uuid
        app_user_id:
          type: string
          minLength: 1
          maxLength: 512
        hint:
          type: string
          maxLength: 80
        expires_at:
          type: string
          format: date-time
        token:
          type: string
          pattern: "^iap_ct_"
          writeOnly: true
      additionalProperties: false
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
      additionalProperties: false
  responses:
    InvalidRequest:
      description: Payload is malformed or fails store validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: The required bearer credential is missing, invalid, or revoked.
      headers:
        WWW-Authenticate:
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    TooLarge:
      description: Request exceeds 256 KiB.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    RateLimited:
      description: The authenticated API key exceeded its process-local receipt rate limit.
      headers:
        Retry-After:
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unavailable:
      description: A verifier or durable dependency is temporarily unavailable.
      headers:
        Retry-After:
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: The app-scoped resource does not exist.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: The requested identifier or lifecycle transition conflicts with existing state.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: The authenticated key or customer token is not authorized for this resource.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  parameters:
    AppID:
      name: app_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    APIKeyID:
      name: key_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    AccessLevelID:
      name: access_level_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    MappingID:
      name: mapping_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ClientTokenID:
      name: token_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CustomerToken:
      name: X-IAP-Customer-Token
      in: header
      required: false
      description: Required with an iap_pk_ publishable key; ignored for an iap_sk_ server key.
      schema:
        type: string
        pattern: "^iap_ct_"
    EndpointID:
      name: endpoint_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    MessageID:
      name: message_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^msg_[A-Za-z0-9_-]{20,64}$"
