# Runtime and admin API reference

This reference is generated from the reviewed OpenAPI contract. It covers all 37 runtime, store-ingress, webhook, health, and root-admin operations and their models. Download the [OpenAPI YAML](/docs/openapi.yaml) for the exact machine-readable contract, including nested/conditional schema constraints.

## Choose the correct API boundary

The staging API origin is `https://api-staging.cherrysub.com`. Operator/local deployments use their own origin. Send JSON with the documented content type. The hosted dashboard's cookie-based `/auth` and `/dashboard` interface is a separate [hosted API](/docs/hosted-api).

- Public client runtime: `iap_pk_` plus `X-IAP-Customer-Token` for the exact app/customer.
- Trusted app server: `iap_sk_`, including token minting and app-scoped catalog/webhook management.
- Root administration: an operator bearer key from `ADMIN_API_KEYS`; it is not a project-member credential.
- Store ingress: verified Apple signed data or authenticated Google Pub/Sub push, not an app bearer key pasted into a notification URL.

Root app creation returns the core app and its first secret key. The hosted project/app creation flow has a different response and project association; use the dashboard/CLI when creating an app in a hosted project.

Runtime error responses use an `error` field. Keep client-facing `event_id` values as decimal strings. Subscriber 404 can mean no app-user alias exists yet; current product-set `null` is an explicit successful empty configuration.

## Operation index

| Method | Path | Operation |
| --- | --- | --- |
| GET | [`/livez`](#get-livez) | getLiveness |
| GET | [`/readyz`](#get-readyz) | getReadiness |
| GET | [`/healthz`](#get-healthz) | getHealth |
| GET | [`/metrics`](#get-metrics) | getMetrics |
| POST | [`/ingest/apple/assn`](#post-ingestappleassn) | ingestAppleNotification |
| POST | [`/ingest/google/rtdn`](#post-ingestgooglertdn) | ingestGoogleNotification |
| POST | [`/v1/receipts`](#post-v1receipts) | submitReceipt |
| GET | [`/v1/subscribers/{app_user_id}`](#get-v1subscribersappuserid) | getSubscriber |
| GET | [`/v1/product-sets/current`](#get-v1product-setscurrent) | getCurrentProductSet |
| GET | [`/v1/webhook-endpoints`](#get-v1webhook-endpoints) | listWebhookEndpoints |
| POST | [`/v1/webhook-endpoints`](#post-v1webhook-endpoints) | createWebhookEndpoint |
| GET | [`/v1/webhook-endpoints/{endpoint_id}`](#get-v1webhook-endpointsendpointid) | getWebhookEndpoint |
| PATCH | [`/v1/webhook-endpoints/{endpoint_id}`](#patch-v1webhook-endpointsendpointid) | updateWebhookEndpoint |
| DELETE | [`/v1/webhook-endpoints/{endpoint_id}`](#delete-v1webhook-endpointsendpointid) | deleteWebhookEndpoint |
| POST | [`/v1/webhook-endpoints/{endpoint_id}/rotate-secret`](#post-v1webhook-endpointsendpointidrotate-secret) | rotateWebhookEndpointSecret |
| GET | [`/v1/webhook-deliveries`](#get-v1webhook-deliveries) | listWebhookDeliveries |
| GET | [`/v1/webhook-deliveries/{message_id}`](#get-v1webhook-deliveriesmessageid) | getWebhookDelivery |
| POST | [`/v1/webhook-deliveries/{message_id}/retry`](#post-v1webhook-deliveriesmessageidretry) | retryWebhookDelivery |
| GET | [`/admin/v1/apps`](#get-adminv1apps) | listApps |
| POST | [`/admin/v1/apps`](#post-adminv1apps) | createApp |
| GET | [`/admin/v1/apps/{app_id}`](#get-adminv1appsappid) | getApp |
| PATCH | [`/admin/v1/apps/{app_id}`](#patch-adminv1appsappid) | updateApp |
| DELETE | [`/admin/v1/apps/{app_id}`](#delete-adminv1appsappid) | disableApp |
| GET | [`/admin/v1/apps/{app_id}/api-keys`](#get-adminv1appsappidapi-keys) | listAppAPIKeys |
| POST | [`/admin/v1/apps/{app_id}/api-keys`](#post-adminv1appsappidapi-keys) | createAppAPIKey |
| DELETE | [`/admin/v1/apps/{app_id}/api-keys/{key_id}`](#delete-adminv1appsappidapi-keyskeyid) | revokeAppAPIKey |
| GET | [`/v1/access-levels`](#get-v1access-levels) | listAccessLevels |
| POST | [`/v1/access-levels`](#post-v1access-levels) | createAccessLevel |
| GET | [`/v1/access-levels/{access_level_id}`](#get-v1access-levelsaccesslevelid) | getAccessLevel |
| PATCH | [`/v1/access-levels/{access_level_id}`](#patch-v1access-levelsaccesslevelid) | updateAccessLevel |
| DELETE | [`/v1/access-levels/{access_level_id}`](#delete-v1access-levelsaccesslevelid) | archiveAccessLevel |
| GET | [`/v1/product-access-mappings`](#get-v1product-access-mappings) | listProductAccessMappings |
| POST | [`/v1/product-access-mappings`](#post-v1product-access-mappings) | createProductAccessMapping |
| PATCH | [`/v1/product-access-mappings/{mapping_id}`](#patch-v1product-access-mappingsmappingid) | updateProductAccessMapping |
| DELETE | [`/v1/product-access-mappings/{mapping_id}`](#delete-v1product-access-mappingsmappingid) | deleteProductAccessMapping |
| POST | [`/v1/client-tokens`](#post-v1client-tokens) | createClientToken |
| DELETE | [`/v1/client-tokens/{token_id}`](#delete-v1client-tokenstokenid) | revokeClientToken |

## Public runtime and trusted catalog

### POST /v1/receipts

Operation: `submitReceipt`. 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.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `X-IAP-Customer-Token` | header | No | string | Required with an iap_pk_ publishable key; ignored for an iap_sk_ server key. pattern: "^iap_ct_". |

Request body: required.

Content type: `application/json`. [ReceiptSubmission](#receiptsubmission)


| Response | Meaning |
| --- | --- |
| 200 | Receipt observation appended or deduplicated. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 403 | The authenticated key or customer token is not authorized for this resource. |
| 413 | Request exceeds 256 KiB. |
| 422 | Payload is malformed or fails store validation. |
| 429 | The authenticated API key exceeded its process-local receipt rate limit. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [IngestResult](#ingestresult)
### GET /v1/subscribers/{app_user_id}

Operation: `getSubscriber`. Returns the current Apple and Google access level union for one app-scoped customer alias.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `app_user_id` | path | Yes | string | minLength: 1. maxLength: 512. |
| `X-IAP-Customer-Token` | header | No | string | Required with an iap_pk_ publishable key; ignored for an iap_sk_ server key. pattern: "^iap_ct_". |

| Response | Meaning |
| --- | --- |
| 200 | Current subscriber access level projection. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 403 | The authenticated key or customer token is not authorized for this resource. |
| 404 | No customer alias exists for this app-user id. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [Subscriber](#subscriber)
### GET /v1/product-sets/current

Operation: `getCurrentProductSet`. Returns the authenticated app's current cross-store product presentation. Publishable keys also require a customer token; secret keys do not.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `X-IAP-Customer-Token` | header | No | string | Required with an iap_pk_ publishable key; ignored for an iap_sk_ server key. pattern: "^iap_ct_". |

| Response | Meaning |
| --- | --- |
| 200 | Current product set, or an explicit null when the app has not selected one. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 403 | The authenticated key or customer token is not authorized for this resource. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [CurrentProductSet](#currentproductset)

Response headers: `Cache-Control` (const: "no-store".).
### GET /v1/access-levels

Operation: `listAccessLevels`. 

Authentication: `bearerAPIKey`


| Response | Meaning |
| --- | --- |
| 200 | Active app-scoped access level catalog. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** 
```json
{
  "type": "object",
  "required": [
    "access_levels"
  ],
  "properties": {
    "access_levels": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/CatalogAccessLevel"
      }
    }
  },
  "additionalProperties": false
}
```

### POST /v1/access-levels

Operation: `createAccessLevel`. 

Authentication: `bearerAPIKey`


Request body: required.

Content type: `application/json`. [CreateAccessLevel](#createaccesslevel)


| Response | Meaning |
| --- | --- |
| 201 | Access level created. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 409 | The requested identifier or lifecycle transition conflicts with existing state. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**201 application/json body:** [CatalogAccessLevel](#catalogaccesslevel)
### GET /v1/access-levels/{access_level_id}

Operation: `getAccessLevel`. 

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `access_level_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 200 | Access level. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [CatalogAccessLevel](#catalogaccesslevel)
### PATCH /v1/access-levels/{access_level_id}

Operation: `updateAccessLevel`. Updates display_name; lookup_key is immutable.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `access_level_id` | path | Yes | string | format: "uuid". |

Request body: required.

Content type: `application/json`. 
```json
{
  "type": "object",
  "required": [
    "display_name"
  ],
  "properties": {
    "display_name": {
      "type": "string",
      "maxLength": 512
    }
  },
  "additionalProperties": false
}
```



| Response | Meaning |
| --- | --- |
| 200 | Access level updated. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [CatalogAccessLevel](#catalogaccesslevel)
### DELETE /v1/access-levels/{access_level_id}

Operation: `archiveAccessLevel`. Archives the lookup key and ends active product access mappings without deleting replay history.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `access_level_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 204 | Access level archived. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |
### GET /v1/product-access-mappings

Operation: `listProductAccessMappings`. 

Authentication: `bearerAPIKey`


| Response | Meaning |
| --- | --- |
| 200 | Active Apple and Google product access mappings. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** 
```json
{
  "type": "object",
  "required": [
    "product_mappings"
  ],
  "properties": {
    "product_mappings": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/ProductAccessMapping"
      }
    }
  },
  "additionalProperties": false
}
```

### POST /v1/product-access-mappings

Operation: `createProductAccessMapping`. Starts a mapping after a short, safe ledger high-water boundary.

Authentication: `bearerAPIKey`


Request body: required.

Content type: `application/json`. [CreateProductAccessMapping](#createproductaccessmapping)


| Response | Meaning |
| --- | --- |
| 201 | Active mapping version. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 409 | The requested identifier or lifecycle transition conflicts with existing state. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**201 application/json body:** [ProductAccessMapping](#productaccessmapping)
### PATCH /v1/product-access-mappings/{mapping_id}

Operation: `updateProductAccessMapping`. Ends the active version and starts a new grant-mode version at one ledger boundary.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `mapping_id` | path | Yes | string | format: "uuid". |

Request body: required.

Content type: `application/json`. 
```json
{
  "type": "object",
  "required": [
    "grant_mode"
  ],
  "properties": {
    "grant_mode": {
      "$ref": "#/components/schemas/GrantMode"
    }
  },
  "additionalProperties": false
}
```



| Response | Meaning |
| --- | --- |
| 200 | New active mapping version. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 409 | The requested identifier or lifecycle transition conflicts with existing state. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [ProductAccessMapping](#productaccessmapping)
### DELETE /v1/product-access-mappings/{mapping_id}

Operation: `deleteProductAccessMapping`. Ends the active version after the current safe ledger head.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `mapping_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 204 | Mapping version ended. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |
### POST /v1/client-tokens

Operation: `createClientToken`. Requires a secret app key. Mints an opaque, short-lived token binding a publishable-key client to one app_user_id.

Authentication: `bearerAPIKey`


Request body: required.

Content type: `application/json`. [CreateClientToken](#createclienttoken)


| Response | Meaning |
| --- | --- |
| 201 | One-time opaque customer token. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 403 | The authenticated key or customer token is not authorized for this resource. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**201 application/json body:** [CreatedClientToken](#createdclienttoken)

Response headers: `Cache-Control` (const: "no-store".).
### DELETE /v1/client-tokens/{token_id}

Operation: `revokeClientToken`. Requires a secret app key and revokes one app-scoped customer token immediately.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `token_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 204 | Client token revoked. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 403 | The authenticated key or customer token is not authorized for this resource. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

## Webhook management and delivery

### GET /v1/webhook-endpoints

Operation: `listWebhookEndpoints`. 

Authentication: `bearerAPIKey`


| Response | Meaning |
| --- | --- |
| 200 | Non-deleted endpoints for the authenticated app. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** 
```json
{
  "type": "object",
  "required": [
    "endpoints"
  ],
  "properties": {
    "endpoints": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/WebhookEndpoint"
      }
    }
  },
  "additionalProperties": false
}
```

### POST /v1/webhook-endpoints

Operation: `createWebhookEndpoint`. 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.

Authentication: `bearerAPIKey`


Request body: required.

Content type: `application/json`. [CreateWebhookEndpoint](#createwebhookendpoint)


| Response | Meaning |
| --- | --- |
| 201 | Endpoint and one-time signing secret. The response must not be cached. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**201 application/json body:** [CreatedWebhookEndpoint](#createdwebhookendpoint)

Response headers: `Cache-Control` (const: "no-store".).
### GET /v1/webhook-endpoints/{endpoint_id}

Operation: `getWebhookEndpoint`. 

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `endpoint_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 200 | Endpoint configuration without signing material. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [WebhookEndpoint](#webhookendpoint)
### PATCH /v1/webhook-endpoints/{endpoint_id}

Operation: `updateWebhookEndpoint`. 

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `endpoint_id` | path | Yes | string | format: "uuid". |

Request body: required.

Content type: `application/json`. [UpdateWebhookEndpoint](#updatewebhookendpoint)


| Response | Meaning |
| --- | --- |
| 200 | Updated endpoint. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [WebhookEndpoint](#webhookendpoint)
### DELETE /v1/webhook-endpoints/{endpoint_id}

Operation: `deleteWebhookEndpoint`. Soft-deletes the endpoint, cancels queued work, and retains delivery history.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `endpoint_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 204 | Endpoint deleted. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |
### POST /v1/webhook-endpoints/{endpoint_id}/rotate-secret

Operation: `rotateWebhookEndpointSecret`. Returns a new signing secret once and dual-signs with the immediately previous secret for 24 hours.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `endpoint_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 200 | One-time rotated secret and overlap boundary. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [RotatedWebhookSecret](#rotatedwebhooksecret)

Response headers: `Cache-Control` (const: "no-store".).
### GET /v1/webhook-deliveries

Operation: `listWebhookDeliveries`. 

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `endpoint_id` | query | No | string | format: "uuid". |
| `status` | query | No | [WebhookDeliveryStatus](#webhookdeliverystatus) |  |
| `before_event_number` | query | No | integer | Exclusive keyset cursor over the monotonic, non-gapless ledger event number. format: "int64". minimum: 1. |
| `limit` | query | No | integer | minimum: 1. maximum: 200. default: 50. |

| Response | Meaning |
| --- | --- |
| 200 | Newest-first delivery history. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** 
```json
{
  "type": "object",
  "required": [
    "deliveries"
  ],
  "properties": {
    "deliveries": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/WebhookDelivery"
      }
    }
  },
  "additionalProperties": false
}
```

### GET /v1/webhook-deliveries/{message_id}

Operation: `getWebhookDelivery`. 

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `message_id` | path | Yes | string | pattern: "^msg_[A-Za-z0-9_-]{20,64}$". |

| Response | Meaning |
| --- | --- |
| 200 | Delivery and every recorded attempt, including expired worker leases. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [WebhookDeliveryDetail](#webhookdeliverydetail)
### POST /v1/webhook-deliveries/{message_id}/retry

Operation: `retryWebhookDelivery`. 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.

Authentication: `bearerAPIKey`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `message_id` | path | Yes | string | pattern: "^msg_[A-Za-z0-9_-]{20,64}$". |

| Response | Meaning |
| --- | --- |
| 200 | Requeued delivery. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 409 | Endpoint is disabled or delivery is already queued/in flight. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [WebhookDelivery](#webhookdelivery)

## Root operator administration

### GET /admin/v1/apps

Operation: `listApps`. 

Authentication: `adminBearer`


| Response | Meaning |
| --- | --- |
| 200 | All active and disabled apps in stable creation order. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** 
```json
{
  "type": "object",
  "required": [
    "apps"
  ],
  "properties": {
    "apps": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/App"
      }
    }
  },
  "additionalProperties": false
}
```

### POST /admin/v1/apps

Operation: `createApp`. Creates an Apple, Google, or cross-platform app and returns its first API key exactly once.

Authentication: `adminBearer`


Request body: required.

Content type: `application/json`. [CreateApp](#createapp)


| Response | Meaning |
| --- | --- |
| 201 | App, safe key metadata, and one-time plaintext secret. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 409 | The requested identifier or lifecycle transition conflicts with existing state. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**201 application/json body:** [CreatedApp](#createdapp)

Response headers: `Cache-Control` (const: "no-store".).
### GET /admin/v1/apps/{app_id}

Operation: `getApp`. 

Authentication: `adminBearer`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `app_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 200 | App configuration. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [App](#app)
### PATCH /admin/v1/apps/{app_id}

Operation: `updateApp`. 

Authentication: `adminBearer`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `app_id` | path | Yes | string | format: "uuid". |

Request body: required.

Content type: `application/json`. [UpdateApp](#updateapp)


| Response | Meaning |
| --- | --- |
| 200 | Updated app. Re-enabling does not un-revoke old keys. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 409 | The requested identifier or lifecycle transition conflicts with existing state. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** [App](#app)
### DELETE /admin/v1/apps/{app_id}

Operation: `disableApp`. Soft-disables ingestion, revokes all app API keys, disables webhooks, and retains audit history.

Authentication: `adminBearer`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `app_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 204 | App disabled. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |
### GET /admin/v1/apps/{app_id}/api-keys

Operation: `listAppAPIKeys`. 

Authentication: `adminBearer`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `app_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 200 | Safe API-key metadata; plaintext keys are never returned. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**200 application/json body:** 
```json
{
  "type": "object",
  "required": [
    "api_keys"
  ],
  "properties": {
    "api_keys": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/APIKey"
      }
    }
  },
  "additionalProperties": false
}
```

### POST /admin/v1/apps/{app_id}/api-keys

Operation: `createAppAPIKey`. 

Authentication: `adminBearer`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `app_id` | path | Yes | string | format: "uuid". |

Request body: required.

Content type: `application/json`. 
```json
{
  "type": "object",
  "properties": {
    "label": {
      "type": "string",
      "maxLength": 512
    },
    "kind": {
      "type": "string",
      "enum": [
        "secret",
        "publishable"
      ],
      "default": "secret"
    }
  },
  "additionalProperties": false
}
```



| Response | Meaning |
| --- | --- |
| 201 | Safe metadata and one-time plaintext secret. |
| 400 | Payload is malformed or fails store validation. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 409 | The requested identifier or lifecycle transition conflicts with existing state. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

**201 application/json body:** [CreatedAPIKey](#createdapikey)

Response headers: `Cache-Control` (const: "no-store".).
### DELETE /admin/v1/apps/{app_id}/api-keys/{key_id}

Operation: `revokeAppAPIKey`. 

Authentication: `adminBearer`

| Parameter | Location | Required | Type | Details |
| --- | --- | --- | --- | --- |
| `app_id` | path | Yes | string | format: "uuid". |
| `key_id` | path | Yes | string | format: "uuid". |

| Response | Meaning |
| --- | --- |
| 204 | Key revoked. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 404 | The app-scoped resource does not exist. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

## Incoming store notifications

### POST /ingest/apple/assn

Operation: `ingestAppleNotification`. Verifies an App Store Server Notifications V2 outer and nested JWS values, attributes its app, and atomically appends and projects or rejects it.

Authentication: No app-key security scheme declared; store-ingress verification still applies where described.


Request body: required.

Content type: `application/json`. 
```json
{
  "type": "object",
  "required": [
    "signedPayload"
  ],
  "properties": {
    "signedPayload": {
      "type": "string"
    }
  },
  "additionalProperties": false
}
```



| Response | Meaning |
| --- | --- |
| 200 | Delivery appended, deduplicated, or durably rejected. |
| 503 | PostgreSQL or Apple's OCSP responder is temporarily unavailable. |
### POST /ingest/google/rtdn

Operation: `ingestGoogleNotification`. Verifies Pub/Sub OIDC identity, appends the RTDN doorbell, fetches authoritative subscriptionsv2 or productsv2 state when required, and atomically projects all line items.

Authentication: `googlePubSubOIDC`


Request body: required.

Content type: `application/json`. [PubSubPush](#pubsubpush)


| Response | Meaning |
| --- | --- |
| 200 | Delivery appended, deduplicated, or durably rejected. |
| 401 | The required bearer credential is missing, invalid, or revoked. |
| 403 | The valid Google identity is not the configured Pub/Sub service account. |
| 503 | A verifier or durable dependency is temporarily unavailable. |

## Health and observability

### GET /livez

Operation: `getLiveness`. 

Authentication: No app-key security scheme declared; store-ingress verification still applies where described.


| Response | Meaning |
| --- | --- |
| 200 | Process is alive. |

**200 application/json body:** [Probe](#probe)
### GET /readyz

Operation: `getReadiness`. 

Authentication: No app-key security scheme declared; store-ingress verification still applies where described.


| Response | Meaning |
| --- | --- |
| 200 | Process and PostgreSQL are ready. |
| 503 | PostgreSQL is unavailable. |

**200 application/json body:** [Probe](#probe)
### GET /healthz

Operation: `getHealth`. Compatibility alias for readiness.

Authentication: No app-key security scheme declared; store-ingress verification still applies where described.


| Response | Meaning |
| --- | --- |
| 200 | Ready. |
| 503 | Not ready. |

**200 application/json body:** [Probe](#probe)
### GET /metrics

Operation: `getMetrics`. 

Authentication: No app-key security scheme declared; store-ingress verification still applies where described.


| Response | Meaning |
| --- | --- |
| 200 | Prometheus exposition. |

**200 text/plain body:** 
```json
{
  "type": "string"
}
```


## Authentication schemes

### googlePubSubOIDC



```json
{
  "type": "http",
  "scheme": "bearer",
  "bearerFormat": "Google OIDC ID token"
}
```

### bearerAPIKey



```json
{
  "type": "http",
  "scheme": "bearer",
  "bearerFormat": "iap_sk_ secret or iap_pk_ publishable app key"
}
```

### adminBearer



```json
{
  "type": "http",
  "scheme": "bearer",
  "bearerFormat": "root admin API key"
}
```


## Request and response models

Fields are listed below. Nested array/object definitions, alternatives, and conditional rules remain in the complete schema following each model when applicable. `writeOnly` denotes one-time/input secret material; do not assume it can be read back from management lists.

### Probe



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `status` | Yes | string | Values: ok, unavailable. |

```json
{
  "type": "object",
  "required": [
    "status"
  ],
  "properties": {
    "status": {
      "type": "string",
      "enum": [
        "ok",
        "unavailable"
      ]
    }
  },
  "additionalProperties": false
}
```

### PubSubPush



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `message` | Yes | object |  |
| `subscription` | No | string |  |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `platform` | Yes | string | Values: apple, google. |
| `app_user_id` | Yes | string | minLength: 1. maxLength: 512. |
| `product_type` | No | string | Required for Google; omitted for Apple. Values: subscription, one_time_product. |
| `store_payload` | Yes | string | Apple StoreKit transaction jwsRepresentation or Google purchase token. |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `event_id` | Yes | string | Positive decimal int64 ledger id (maximum 9223372036854775807) encoded as a string for lossless JavaScript and Dart interoperability. maxLength: 19. pattern: "^[1-9][0-9]*$". |
| `deduplicated` | Yes | boolean |  |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `app_user_id` | Yes | string |  |
| `customer_id` | Yes | string | format: "uuid". |
| `access_levels` | Yes | object |  |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `active` | Yes | boolean |  |
| `expires_at` | No | string | format: "date-time". |
| `platform` | Yes | string | Values: apple, google. |
| `product_id` | Yes | string |  |
| `last_verified_at` | Yes | string | format: "date-time". |
| `event_id` | Yes | string | Positive decimal int64 ledger id (maximum 9223372036854775807) encoded as a string for lossless JavaScript and Dart interoperability. maxLength: 19. pattern: "^[1-9][0-9]*$". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `product_set` | Yes | union |  |

```json
{
  "type": "object",
  "required": [
    "product_set"
  ],
  "properties": {
    "product_set": {
      "anyOf": [
        {
          "$ref": "#/components/schemas/ProductSet"
        },
        {
          "type": "null"
        }
      ]
    }
  },
  "additionalProperties": false
}
```

### ProductSet



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | format: "uuid". |
| `identifier` | Yes | string | minLength: 1. maxLength: 128. pattern: "^[A-Za-z0-9._-]+$". |
| `display_name` | Yes | string | minLength: 1. maxLength: 256. |
| `packages` | Yes | array of [ProductSetPackage](#productsetpackage) | Ordered by ascending position; identifiers and positions are unique within the set. |
| `updated_at` | Yes | string | format: "date-time". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | format: "uuid". |
| `identifier` | Yes | string | minLength: 1. maxLength: 128. pattern: "^[A-Za-z0-9._-]+$". |
| `package_type` | Yes | string | Values: custom, lifetime, annual, six_month, three_month, two_month, monthly, weekly. |
| `platform` | Yes | string | Values: apple, google. |
| `product_id` | Yes | string | minLength: 1. maxLength: 512. |
| `base_plan_id` | No | string | Optional Google Play base-plan identifier. Present only for Google packages; preserve it when selecting a storefront offer. minLength: 1. maxLength: 128. pattern: "^[A-Za-z0-9._-]+$". |
| `position` | Yes | integer | minimum: 0. maximum: 1000. |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | format: "uuid". |
| `url` | Yes | string | format: "uri". maxLength: 2048. |
| `description` | Yes | string | maxLength: 512. |
| `event_types` | Yes | array of string |  |
| `enabled` | Yes | boolean |  |
| `disabled_reason` | No | string | maxLength: 512. |
| `disabled_at` | No | string | format: "date-time". |
| `created_at` | Yes | string | format: "date-time". |
| `updated_at` | Yes | string | format: "date-time". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `url` | Yes | string | format: "uri". maxLength: 2048. pattern: "^https://". |
| `description` | No | string | maxLength: 512. |
| `event_types` | No | array of string | default: ["access.changed"]. |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `url` | No | string | format: "uri". maxLength: 2048. pattern: "^https://". |
| `description` | No | string | maxLength: 512. |
| `event_types` | No | array of string |  |
| `enabled` | No | boolean |  |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `endpoint` | Yes | [WebhookEndpoint](#webhookendpoint) |  |
| `secret` | Yes | string | pattern: "^whsec_". writeOnly: true. |

```json
{
  "type": "object",
  "required": [
    "endpoint",
    "secret"
  ],
  "properties": {
    "endpoint": {
      "$ref": "#/components/schemas/WebhookEndpoint"
    },
    "secret": {
      "type": "string",
      "pattern": "^whsec_",
      "writeOnly": true
    }
  },
  "additionalProperties": false
}
```

### RotatedWebhookSecret



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `endpoint_id` | Yes | string | format: "uuid". |
| `secret` | Yes | string | pattern: "^whsec_". writeOnly: true. |
| `rotated_at` | Yes | string | format: "date-time". |
| `overlap_ends_at` | Yes | string | format: "date-time". |

```json
{
  "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




```json
{
  "type": "string",
  "enum": [
    "pending",
    "in_flight",
    "retry",
    "succeeded",
    "dead_letter",
    "canceled"
  ]
}
```

### WebhookDelivery



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `message_id` | Yes | string | pattern: "^msg_[A-Za-z0-9_-]{20,64}$". |
| `endpoint_id` | Yes | string | format: "uuid". |
| `event_number` | Yes | integer | format: "int64". |
| `event_type` | Yes | string |  |
| `status` | Yes | [WebhookDeliveryStatus](#webhookdeliverystatus) |  |
| `attempt_count` | Yes | integer | minimum: 0. |
| `next_attempt_at` | No | string | format: "date-time". |
| `first_attempt_at` | No | string | format: "date-time". |
| `last_attempt_at` | No | string | format: "date-time". |
| `succeeded_at` | No | string | format: "date-time". |
| `last_http_status` | No | integer | minimum: 100. maximum: 599. |
| `last_error` | No | string | maxLength: 2048. |
| `last_response_body` | No | string | UTF-8-safe preview capped at 64 KiB. |
| `cycle_started_at` | Yes | string | format: "date-time". |
| `created_at` | Yes | string | format: "date-time". |
| `updated_at` | Yes | string | format: "date-time". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `attempt_number` | Yes | integer | minimum: 1. |
| `started_at` | Yes | string | format: "date-time". |
| `finished_at` | No | string | format: "date-time". |
| `http_status` | No | integer | minimum: 100. maximum: 599. |
| `error` | No | string | maxLength: 2048. |
| `response_body` | No | string |  |
| `duration_ms` | No | integer | format: "int64". minimum: 0. |
| `succeeded` | No | boolean |  |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `delivery` | Yes | [WebhookDelivery](#webhookdelivery) |  |
| `attempts` | Yes | array of [WebhookDeliveryAttempt](#webhookdeliveryattempt) |  |

```json
{
  "type": "object",
  "required": [
    "delivery",
    "attempts"
  ],
  "properties": {
    "delivery": {
      "$ref": "#/components/schemas/WebhookDelivery"
    },
    "attempts": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/WebhookDeliveryAttempt"
      }
    }
  },
  "additionalProperties": false
}
```

### OutboundWebhookEvent

Exact JSON body signed using Standard Webhooks headers.

| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | pattern: "^evt_[0-9]+$". |
| `type` | Yes | string |  |
| `api_version` | Yes | string | const: "2026-08-28". |
| `timestamp` | Yes | string | format: "date-time". |
| `event_number` | Yes | integer | Monotonic but not gapless ledger identifier. format: "int64". |
| `data` | Yes | object |  |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | format: "uuid". |
| `name` | Yes | string | minLength: 1. maxLength: 512. |
| `apple_bundle_id` | No | string | minLength: 1. maxLength: 512. |
| `apple_app_apple_id` | No | integer | format: "int64". minimum: 1. |
| `google_package_name` | No | string | minLength: 1. maxLength: 512. |
| `enabled` | Yes | boolean |  |
| `disabled_at` | No | string | format: "date-time". |
| `created_at` | Yes | string | format: "date-time". |
| `updated_at` | Yes | string | format: "date-time". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `name` | Yes | string | minLength: 1. maxLength: 512. |
| `apple_bundle_id` | No | string | minLength: 1. maxLength: 512. |
| `apple_app_apple_id` | No | integer | Required for production Apple notification attribution. format: "int64". minimum: 1. |
| `google_package_name` | No | string | minLength: 1. maxLength: 512. |
| `initial_key_label` | No | string | maxLength: 512. default: "default". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `name` | No | string | minLength: 1. maxLength: 512. |
| `apple_bundle_id` | No | string or null | minLength: 1. maxLength: 512. |
| `apple_app_apple_id` | No | integer or null | format: "int64". minimum: 1. |
| `google_package_name` | No | string or null | minLength: 1. maxLength: 512. |
| `enabled` | No | boolean |  |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | format: "uuid". |
| `kind` | Yes | string | Values: secret, publishable. |
| `label` | Yes | string | maxLength: 512. |
| `hint` | Yes | string | maxLength: 80. |
| `created_at` | Yes | string | format: "date-time". |
| `revoked_at` | No | string | format: "date-time". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `api_key` | Yes | [APIKey](#apikey) |  |
| `secret` | Yes | string | pattern: "^iap_(sk\|pk)_". writeOnly: true. |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `app` | Yes | [App](#app) |  |
| `api_key` | Yes | [APIKey](#apikey) |  |
| `secret` | Yes | string | pattern: "^iap_sk_". writeOnly: true. |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | format: "uuid". |
| `lookup_key` | Yes | string | pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$". |
| `display_name` | Yes | string | maxLength: 512. |
| `created_at` | Yes | string | format: "date-time". |
| `updated_at` | Yes | string | format: "date-time". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `lookup_key` | Yes | string | pattern: "^[A-Za-z0-9][A-Za-z0-9._-]{0,254}$". |
| `display_name` | No | string | maxLength: 512. |

```json
{
  "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




```json
{
  "type": "string",
  "enum": [
    "store_managed",
    "lifetime"
  ]
}
```

### ProductAccessMapping



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | format: "uuid". |
| `platform` | Yes | string | Values: apple, google. |
| `product_id` | Yes | string | minLength: 1. maxLength: 512. |
| `access_level_id` | Yes | string | format: "uuid". |
| `access_level_key` | Yes | string |  |
| `grant_mode` | Yes | [GrantMode](#grantmode) |  |
| `starts_after_event_id` | Yes | integer | Mapping applies to source events with a greater ledger id. format: "int64". minimum: 0. |
| `created_at` | Yes | string | format: "date-time". |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `platform` | Yes | string | Values: apple, google. |
| `product_id` | Yes | string | minLength: 1. maxLength: 512. |
| `access_level_id` | Yes | string | format: "uuid". |
| `grant_mode` | No | [GrantMode](#grantmode) |  |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `app_user_id` | Yes | string | minLength: 1. maxLength: 512. |
| `ttl_seconds` | No | integer | format: "int64". minimum: 300. maximum: 86400. default: 3600. |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `id` | Yes | string | format: "uuid". |
| `app_user_id` | Yes | string | minLength: 1. maxLength: 512. |
| `hint` | Yes | string | maxLength: 80. |
| `expires_at` | Yes | string | format: "date-time". |
| `token` | Yes | string | pattern: "^iap_ct_". writeOnly: true. |

```json
{
  "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



| Field | Required | Type | Details |
| --- | --- | --- | --- |
| `error` | Yes | string |  |

```json
{
  "type": "object",
  "required": [
    "error"
  ],
  "properties": {
    "error": {
      "type": "string"
    }
  },
  "additionalProperties": false
}
```


## Shared error responses

### InvalidRequest

Payload is malformed or fails store validation.

```json
{
  "description": "Payload is malformed or fails store validation.",
  "content": {
    "application/json": {
      "schema": {
        "$ref": "#/components/schemas/Error"
      }
    }
  }
}
```

### Unauthorized

The required bearer credential is missing, invalid, or revoked.

```json
{
  "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

Request exceeds 256 KiB.

```json
{
  "description": "Request exceeds 256 KiB.",
  "content": {
    "application/json": {
      "schema": {
        "$ref": "#/components/schemas/Error"
      }
    }
  }
}
```

### RateLimited

The authenticated API key exceeded its process-local receipt rate limit.

```json
{
  "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

A verifier or durable dependency is temporarily unavailable.

```json
{
  "description": "A verifier or durable dependency is temporarily unavailable.",
  "headers": {
    "Retry-After": {
      "schema": {
        "type": "string"
      }
    }
  },
  "content": {
    "application/json": {
      "schema": {
        "$ref": "#/components/schemas/Error"
      }
    }
  }
}
```

### NotFound

The app-scoped resource does not exist.

```json
{
  "description": "The app-scoped resource does not exist.",
  "content": {
    "application/json": {
      "schema": {
        "$ref": "#/components/schemas/Error"
      }
    }
  }
}
```

### Conflict

The requested identifier or lifecycle transition conflicts with existing state.

```json
{
  "description": "The requested identifier or lifecycle transition conflicts with existing state.",
  "content": {
    "application/json": {
      "schema": {
        "$ref": "#/components/schemas/Error"
      }
    }
  }
}
```

### Forbidden

The authenticated key or customer token is not authorized for this resource.

```json
{
  "description": "The authenticated key or customer token is not authorized for this resource.",
  "content": {
    "application/json": {
      "schema": {
        "$ref": "#/components/schemas/Error"
      }
    }
  }
}
```

