{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "Rovenue API",
    "version": "1.0.0",
    "description": "The public Rovenue `/v1` API — the surface the mobile and web SDKs talk to.\n\n**How much of this document you can trust blindly.** Half of it is generated by walking the running Hono route table (`apps/api/scripts/generate-openapi.ts`) and half is written by hand, and the two halves fail differently:\n\n- **Generated, cannot drift:** which endpoints exist, every request body (converted from the Zod schema the route actually validates against), and every authentication requirement (read from the middleware chain, resolved in Hono's own registration order).\n- **Hand-maintained, can drift:** all response bodies, all parameters (path, query, header), and all prose. Nothing in the codebase describes these as data — `ok<T>(data)` is an identity generic erased at runtime, and no route uses `validate(\"query\")` or `validate(\"param\")` — so generating them would mean inventing them. A contract test pins the endpoint *set* so nothing can appear or vanish undocumented, but it cannot notice a response field that changed shape.\n\n**Authentication.** Every endpoint is gated by a per-project API key presented as `Authorization: Bearer <key>`. Public keys (`rov_pub_…`) ship inside your app; secret keys (`rov_sec_…`) must never leave your server. Endpoints marked with only `secretApiKey` reject a public key with 403, and vice versa.\n\n**Envelope.** Successful responses are `{ \"data\": … }`; failures are `{ \"error\": { \"code\", \"message\" } }`. Two funnel endpoints return the *success* envelope with a null payload on 404/202 rather than the error envelope; both say so.\n\n**Browser surface.** The same router is mounted a second time under `/v1/web/{publicKey}` for browser callers, because a CORS preflight carries no `Authorization` header and the project has to be resolvable from the URL alone. It is not a second set of endpoints and is not listed as one — see `x-rovenue-browser-surface`.",
    "license": {
      "name": "AGPL-3.0",
      "identifier": "AGPL-3.0-only"
    }
  },
  "servers": [
    {
      "url": "/",
      "description": "Relative to your own Rovenue deployment (e.g. https://api.example.com). Rovenue is self-hosted; there is no canonical host."
    }
  ],
  "tags": [
    {
      "name": "Commerce",
      "description": "Store receipts, offerings and the Stripe checkout/billing-portal handoff."
    },
    {
      "name": "Data subject requests",
      "description": "GDPR/KVKK data subject requests: open an export or erasure for a subscriber, poll its status, and download the finished artifact. Every route requires the project SECRET key, never the public one — these act on a subscriber's behalf and must not be reachable from a shipped app bundle."
    },
    {
      "name": "Experiments",
      "description": "Exposure and conversion recording, and the ClickHouse-backed results read."
    },
    {
      "name": "Funnels",
      "description": "Claiming a web-funnel purchase onto an app install."
    },
    {
      "name": "Placements",
      "description": "Resolving a placement to the funnel a subscriber should see, plus the font-delivery endpoint that supports it."
    },
    {
      "name": "Remote config",
      "description": "Per-subscriber feature flags and experiment assignments, by request or over a live SSE stream."
    },
    {
      "name": "Subscribers",
      "description": "Reading and mutating subscriber identity, attributes and entitlement access. `/v1/me/*` resolves the subscriber from a header; the `/v1/subscribers/*` family names them in the path."
    },
    {
      "name": "Telemetry",
      "description": "Fire-and-forget event ingestion. Both endpoints answer 202 with no body and require a PUBLIC key."
    },
    {
      "name": "Virtual currencies",
      "description": "Virtual-currency balances and ledger transactions. Grants and spends are server-side only."
    }
  ],
  "paths": {
    "/v1/billing-portal": {
      "post": {
        "operationId": "postV1Billingportal",
        "summary": "Create a Stripe billing-portal session",
        "tags": [
          "Commerce"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Portal session created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "description": "Stripe billing-portal session. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "X-Rovenue-App-User-Id",
            "in": "header",
            "required": true,
            "description": "Device key (the SDK's permanent `rovenueId`). `appUserContext` resolves or creates the subscriber from it; absent, the request is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-Platform",
            "in": "header",
            "required": false,
            "description": "First-install platform (`ios`, `android`, `web`). Persisted only when this call creates the subscriber; ignored otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "ios",
                "android",
                "web"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "returnUrl": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "required": [
                  "returnUrl"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/checkout": {
      "post": {
        "operationId": "postV1Checkout",
        "summary": "Create a Stripe Checkout session",
        "tags": [
          "Commerce"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Checkout session created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "description": "Stripe Checkout session. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, or the project has no connected Stripe account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "X-Rovenue-App-User-Id",
            "in": "header",
            "required": true,
            "description": "Device key (the SDK's permanent `rovenueId`). `appUserContext` resolves or creates the subscriber from it; absent, the request is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-Platform",
            "in": "header",
            "required": false,
            "description": "First-install platform (`ios`, `android`, `web`). Persisted only when this call creates the subscriber; ignored otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "ios",
                "android",
                "web"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "offeringIdentifier": {
                    "type": "string",
                    "minLength": 1
                  },
                  "packageIdentifier": {
                    "type": "string",
                    "minLength": 1
                  },
                  "successUrl": {
                    "type": "string",
                    "format": "uri"
                  },
                  "cancelUrl": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "required": [
                  "offeringIdentifier",
                  "packageIdentifier",
                  "successUrl",
                  "cancelUrl"
                ],
                "additionalProperties": false
              }
            }
          }
        }
      }
    },
    "/v1/config": {
      "get": {
        "operationId": "getV1Config",
        "summary": "Evaluate remote config for a subscriber",
        "tags": [
          "Remote config"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Evaluated config.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "flags",
                        "experiments"
                      ],
                      "properties": {
                        "flags": {
                          "description": "Evaluated feature flags, keyed by flag key. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        },
                        "experiments": {
                          "description": "Experiment assignments for this subscriber. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Returns the subscriber's evaluated feature flags and experiment assignments.",
        "parameters": [
          {
            "name": "subscriberId",
            "in": "query",
            "required": false,
            "description": "SDK wire identity of the subscriber. Required unless supplied via the `X-Rovenue-User-Id` header; a request carrying neither is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-User-Id",
            "in": "header",
            "required": false,
            "description": "SDK wire identity of the subscriber. Fallback for the `subscriberId` query parameter; the query parameter wins when both are present.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "env",
            "in": "query",
            "required": false,
            "description": "Feature-flag environment to evaluate against. Defaults to `prod` when absent from both the query and the `X-Rovenue-Env` header.",
            "schema": {
              "type": "string",
              "enum": [
                "prod",
                "production",
                "PROD",
                "PRODUCTION",
                "staging",
                "STAGING",
                "development",
                "dev",
                "DEVELOPMENT",
                "DEV"
              ]
            }
          },
          {
            "name": "X-Rovenue-Env",
            "in": "header",
            "required": false,
            "description": "Fallback for the `env` query parameter.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "postV1Config",
        "summary": "Evaluate remote config with request-scoped attributes",
        "tags": [
          "Remote config"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Evaluated config.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "flags",
                        "experiments"
                      ],
                      "properties": {
                        "flags": {
                          "description": "Evaluated feature flags, keyed by flag key. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        },
                        "experiments": {
                          "description": "Experiment assignments for this subscriber. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Identical to `GET /v1/config`, but the body carries attributes that participate in audience evaluation for this call AND are persisted on the subscriber (same validation as the attribute endpoints — malformed keys, unknown `$`-reserved keys, oversized values, and custom-cap overflow are rejected with 400). Attributes are SDK-asserted and untrusted: never gate monetary value on flags/funnels alone — re-check entitlements server-side at purchase/redemption.",
        "parameters": [
          {
            "name": "subscriberId",
            "in": "query",
            "required": false,
            "description": "SDK wire identity of the subscriber. Required unless supplied via the `X-Rovenue-User-Id` header; a request carrying neither is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-User-Id",
            "in": "header",
            "required": false,
            "description": "SDK wire identity of the subscriber. Fallback for the `subscriberId` query parameter; the query parameter wins when both are present.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "env",
            "in": "query",
            "required": false,
            "description": "Feature-flag environment to evaluate against. Defaults to `prod` when absent from both the query and the `X-Rovenue-Env` header.",
            "schema": {
              "type": "string",
              "enum": [
                "prod",
                "production",
                "PROD",
                "PRODUCTION",
                "staging",
                "STAGING",
                "development",
                "dev",
                "DEVELOPMENT",
                "DEV"
              ]
            }
          },
          {
            "name": "X-Rovenue-Env",
            "in": "header",
            "required": false,
            "description": "Fallback for the `env` query parameter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "attributes": {
                    "type": "object",
                    "additionalProperties": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/config/stream": {
      "get": {
        "operationId": "getV1ConfigStream",
        "summary": "Stream remote config over SSE",
        "tags": [
          "Remote config"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "An open `text/event-stream`. Event names: `initial`, `invalidate`, `ping`. The `data` of `initial`/`invalidate` is `{ flags, experiments, projectId }` — note this is NOT wrapped in the `{ data }` envelope.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Server-sent events carrying the same evaluated `{ flags, experiments }` payload as `GET /v1/config`: an `initial` event on connect, an `invalidate` event whenever the project's flag/experiment/audience config changes, and a `ping` keepalive every 25s. The connection is held open until the client disconnects.\n\nMounted at the root app rather than inside `v1Route` (see `routes/v1/config-stream.ts`), and it registers its OWN `apiKeyAuth(\"any\")`. Because it is registered *after* the `/v1` mount, the `v1Route` envelope's `apiKeyAuth` and per-key rate limiter also apply — so the key check runs twice.",
        "parameters": [
          {
            "name": "subscriberId",
            "in": "query",
            "required": false,
            "description": "SDK wire identity of the subscriber. Required unless supplied via the `X-Rovenue-User-Id` header; a request carrying neither is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-User-Id",
            "in": "header",
            "required": false,
            "description": "SDK wire identity of the subscriber. Fallback for the `subscriberId` query parameter; the query parameter wins when both are present.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "env",
            "in": "query",
            "required": false,
            "description": "Feature-flag environment to evaluate against. Defaults to `prod` when absent from both the query and the `X-Rovenue-Env` header.",
            "schema": {
              "type": "string",
              "enum": [
                "prod",
                "production",
                "PROD",
                "PRODUCTION",
                "staging",
                "STAGING",
                "development",
                "dev",
                "DEVELOPMENT",
                "DEV"
              ]
            }
          },
          {
            "name": "X-Rovenue-Env",
            "in": "header",
            "required": false,
            "description": "Fallback for the `env` query parameter.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/dsar/{id}": {
      "get": {
        "operationId": "getV1DsarById",
        "summary": "Read one DSAR request's status",
        "tags": [
          "Data subject requests"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The DSAR request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "request"
                      ],
                      "properties": {
                        "request": {
                          "type": "object",
                          "required": [
                            "id",
                            "type",
                            "status",
                            "requestedBy",
                            "downloadReady"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "type": {
                              "type": "string",
                              "enum": [
                                "EXPORT",
                                "ERASURE"
                              ]
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "PENDING",
                                "RUNNING",
                                "COMPLETED",
                                "FAILED"
                              ]
                            },
                            "requestedBy": {
                              "type": "string"
                            },
                            "createdAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "updatedAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "completedAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "expiresAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "error": {
                              "type": "string",
                              "nullable": true
                            },
                            "downloadReady": {
                              "type": "boolean",
                              "description": "True only when the artifact can be fetched right now: status COMPLETED, an artifact present, and not past expiresAt."
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No DSAR request with this id belongs to the calling project. Returned for another project's id too, deliberately.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Server-to-server only (secret key). Scoped to the calling project: a request belonging to another project is reported as 404, never as 403, so the endpoint does not confirm the existence of other projects' ids.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The DSAR request id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/dsar/{id}/download": {
      "get": {
        "operationId": "getV1DsarByIdDownload",
        "summary": "Download a completed export artifact",
        "tags": [
          "Data subject requests"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The export artifact, streamed. Not JSON — an opaque binary body.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No download is available: the request is not COMPLETED, has no artifact, the link has expired, or the object is no longer in storage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Server-to-server only (secret key). Streams the artifact as `application/octet-stream` with a `Content-Disposition` attachment filename — this is the one `/v1` response that is not a JSON envelope. Returns 404, not 410, for every unavailable case: the request is not COMPLETED, it has no artifact, the link has expired, or the object is gone from storage. Check `downloadReady` on the request first.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The DSAR request id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/dsar/erasure": {
      "post": {
        "operationId": "postV1DsarErasure",
        "summary": "Open an erasure request for a subscriber",
        "tags": [
          "Data subject requests"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The DSAR request, newly created or already open.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "request"
                      ],
                      "properties": {
                        "request": {
                          "type": "object",
                          "required": [
                            "id",
                            "type",
                            "status",
                            "requestedBy",
                            "downloadReady"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "type": {
                              "type": "string",
                              "enum": [
                                "EXPORT",
                                "ERASURE"
                              ]
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "PENDING",
                                "RUNNING",
                                "COMPLETED",
                                "FAILED"
                              ]
                            },
                            "requestedBy": {
                              "type": "string"
                            },
                            "createdAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "updatedAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "completedAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "expiresAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "error": {
                              "type": "string",
                              "nullable": true
                            },
                            "downloadReady": {
                              "type": "boolean",
                              "description": "True only when the artifact can be fetched right now: status COMPLETED, an artifact present, and not past expiresAt."
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No subscriber matches the supplied `appUserId` in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Server-to-server only (secret key). Enqueues an erasure job and returns the request row. Same single-open-request semantics as the export route.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "appUserId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "requestedBy": {
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "appUserId",
                  "requestedBy"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/dsar/export": {
      "post": {
        "operationId": "postV1DsarExport",
        "summary": "Open a data-export request for a subscriber",
        "tags": [
          "Data subject requests"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The DSAR request, newly created or already open.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "request"
                      ],
                      "properties": {
                        "request": {
                          "type": "object",
                          "required": [
                            "id",
                            "type",
                            "status",
                            "requestedBy",
                            "downloadReady"
                          ],
                          "properties": {
                            "id": {
                              "type": "string"
                            },
                            "type": {
                              "type": "string",
                              "enum": [
                                "EXPORT",
                                "ERASURE"
                              ]
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "PENDING",
                                "RUNNING",
                                "COMPLETED",
                                "FAILED"
                              ]
                            },
                            "requestedBy": {
                              "type": "string"
                            },
                            "createdAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "updatedAt": {
                              "type": "string",
                              "format": "date-time"
                            },
                            "completedAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "expiresAt": {
                              "type": "string",
                              "format": "date-time",
                              "nullable": true
                            },
                            "error": {
                              "type": "string",
                              "nullable": true
                            },
                            "downloadReady": {
                              "type": "boolean",
                              "description": "True only when the artifact can be fetched right now: status COMPLETED, an artifact present, and not past expiresAt."
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No subscriber matches the supplied `appUserId` in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Server-to-server only (secret key). Enqueues an export job and returns the request row. Opening a second request of the same type while one is still PENDING or RUNNING returns the request already open rather than creating a duplicate, so the call is safe to retry.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "appUserId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "requestedBy": {
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "appUserId",
                  "requestedBy"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/events": {
      "post": {
        "operationId": "postV1Events",
        "summary": "Ingest an SDK event",
        "tags": [
          "Telemetry"
        ],
        "security": [
          {
            "publicApiKey": []
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted. Empty body."
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The presented key is the wrong kind for this endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Writes an `outbox_events` row inside the request transaction; the dispatcher publishes to Kafka. This is the only path to Kafka — nothing here dual-writes.\n\nA native (device) funnel event has an `eventType` beginning with `funnel_` plus a `funnelContext` object (`funnelId`, `sessionId`, the placement keys, `revision` and `screenId`). Four `eventType` values enter the funnel event vocabulary and can reach an outbound webhook or chat integration once emitted: `funnel_view` becomes a session start, `funnel_screen_view` becomes a forward arrival (to the screen named in `funnelContext.screenId`), `funnel_complete` becomes a session completion, and `funnel_dismiss` becomes a native-only dismissal. Any other `funnel_<x>` value is accepted and stored the same way, but is kept out of the funnel event vocabulary entirely and never reaches a webhook or chat integration.\n\nA body in the pre-rename device vocabulary is refused with a 400 — there is no compat path and no SDK-version branching. An old build meets ordinary validation, never a 500.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "version": {
                    "type": "number",
                    "const": 1
                  },
                  "eventId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "eventType": {
                    "type": "string",
                    "minLength": 1
                  },
                  "occurredAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "subscriberId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "productId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "amount": {
                    "type": "string",
                    "pattern": "^-?\\d+(\\.\\d+)?$"
                  },
                  "currency": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 3
                  },
                  "eventSourceUrl": {
                    "type": "string",
                    "format": "uri"
                  },
                  "identityContext": {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "minLength": 1
                      },
                      "externalId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "phone": {
                        "type": "string",
                        "minLength": 1
                      },
                      "ip": {
                        "type": "string",
                        "minLength": 1
                      },
                      "userAgent": {
                        "type": "string",
                        "minLength": 1
                      },
                      "firstName": {
                        "type": "string",
                        "minLength": 1
                      },
                      "lastName": {
                        "type": "string",
                        "minLength": 1
                      },
                      "city": {
                        "type": "string",
                        "minLength": 1
                      },
                      "countryCode": {
                        "type": "string",
                        "minLength": 1
                      }
                    },
                    "additionalProperties": false
                  },
                  "funnelContext": {
                    "type": "object",
                    "properties": {
                      "funnelId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "sessionId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "placementId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "placementRevision": {
                        "type": "integer",
                        "exclusiveMinimum": 0
                      },
                      "revision": {
                        "type": "integer",
                        "exclusiveMinimum": 0
                      },
                      "screenId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "variantId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "experimentKey": {
                        "type": "string",
                        "minLength": 1
                      }
                    },
                    "required": [
                      "funnelId",
                      "sessionId",
                      "placementId",
                      "placementRevision",
                      "revision"
                    ],
                    "additionalProperties": true
                  }
                },
                "required": [
                  "eventType",
                  "occurredAt"
                ],
                "additionalProperties": false,
                "description": "Note: this endpoint carries a Zod refinement or transform, which JSON Schema cannot express — the server enforces at least one cross-field rule this body schema does not state."
              }
            }
          }
        }
      }
    },
    "/v1/experiments/{id}/expose": {
      "post": {
        "operationId": "postV1ExperimentsByIdExpose",
        "summary": "Record an experiment exposure",
        "tags": [
          "Experiments"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Exposure accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "accepted"
                      ],
                      "properties": {
                        "accepted": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Experiment id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "variantId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "subscriberId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "placementId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "platform": {
                    "type": "string",
                    "enum": [
                      "ios",
                      "android",
                      "web"
                    ]
                  },
                  "country": {
                    "type": "string",
                    "minLength": 2,
                    "maxLength": 2
                  },
                  "exposedAt": {
                    "type": "string",
                    "format": "date-time"
                  }
                },
                "required": [
                  "variantId",
                  "subscriberId"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/experiments/{id}/results": {
      "get": {
        "operationId": "getV1ExperimentsByIdResults",
        "summary": "Fetch experiment results",
        "tags": [
          "Experiments"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Results.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "description": "Experiment result statistics. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No such experiment in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "ClickHouse-backed statistics (SRM plus per-variant exposures). Scoped to the authenticated project, so a cross-project id surfaces as 'experiment not found' rather than leaking existence.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Experiment id.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/experiments/track": {
      "post": {
        "operationId": "postV1ExperimentsTrack",
        "summary": "Record experiment conversion events",
        "tags": [
          "Experiments"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Events recorded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "recorded"
                      ],
                      "properties": {
                        "recorded": {
                          "type": "integer",
                          "description": "Number of events accepted from the batch."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "subscriberId",
            "in": "query",
            "required": false,
            "description": "SDK wire identity of the subscriber. Required unless supplied via the `X-Rovenue-User-Id` header; a request carrying neither is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-User-Id",
            "in": "header",
            "required": false,
            "description": "SDK wire identity of the subscriber. Fallback for the `subscriberId` query parameter; the query parameter wins when both are present.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1
                        },
                        "type": {
                          "type": "string",
                          "minLength": 1
                        },
                        "timestamp": {
                          "type": "string"
                        },
                        "metadata": {
                          "type": "object",
                          "additionalProperties": {}
                        }
                      },
                      "required": [
                        "type"
                      ],
                      "additionalProperties": true
                    },
                    "minItems": 1
                  }
                },
                "required": [
                  "events"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/fonts/{faceId}/{contentHash}/file": {
      "get": {
        "operationId": "getV1FontsByFaceIdByContentHashFile",
        "summary": "Download a font face",
        "tags": [
          "Placements"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The raw font bytes. Content-Type is the face's format (woff2/woff/ttf/otf), or `application/octet-stream` when unrecognised.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            },
            "headers": {
              "ETag": {
                "description": "The face's content hash, quoted.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "description": "`public, max-age=…, immutable`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No such face, wrong content hash, or the face belongs to another project — all reported identically.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Content-addressed: the response is `immutable` for a year, so a changed face is a changed URL.",
        "parameters": [
          {
            "name": "faceId",
            "in": "path",
            "required": true,
            "description": "Font face id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contentHash",
            "in": "path",
            "required": true,
            "description": "Content hash of the face. A mismatch is reported as 404, not as a redirect.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/identify": {
      "post": {
        "operationId": "postV1Identify",
        "summary": "Resolve or create a subscriber for the SDK's wire identity",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The resolved subscriber.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "description": "Resolved subscriber. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rovenueId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "appUserId": {
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "rovenueId",
                  "appUserId"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "operationId": "getV1Me",
        "summary": "Fetch the calling subscriber and their access map",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The subscriber and their entitlement access map.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "subscriber",
                        "access"
                      ],
                      "properties": {
                        "subscriber": {
                          "$ref": "#/components/schemas/SubscriberSummary"
                        },
                        "access": {
                          "description": "Denormalised entitlement access map, keyed by entitlement identifier. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "X-Rovenue-App-User-Id",
            "in": "header",
            "required": true,
            "description": "Device key (the SDK's permanent `rovenueId`). `appUserContext` resolves or creates the subscriber from it; absent, the request is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-Platform",
            "in": "header",
            "required": false,
            "description": "First-install platform (`ios`, `android`, `web`). Persisted only when this call creates the subscriber; ignored otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "ios",
                "android",
                "web"
              ]
            }
          }
        ]
      }
    },
    "/v1/me/access": {
      "get": {
        "operationId": "getV1MeAccess",
        "summary": "Fetch the calling subscriber's access map",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Entitlement access map.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "access"
                      ],
                      "properties": {
                        "access": {
                          "description": "Denormalised entitlement access map, keyed by entitlement identifier. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "X-Rovenue-App-User-Id",
            "in": "header",
            "required": true,
            "description": "Device key (the SDK's permanent `rovenueId`). `appUserContext` resolves or creates the subscriber from it; absent, the request is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-Platform",
            "in": "header",
            "required": false,
            "description": "First-install platform (`ios`, `android`, `web`). Persisted only when this call creates the subscriber; ignored otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "ios",
                "android",
                "web"
              ]
            }
          }
        ]
      }
    },
    "/v1/me/attributes": {
      "post": {
        "operationId": "postV1MeAttributes",
        "summary": "Merge custom attributes onto the calling subscriber",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The subscriber with merged attributes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "subscriber"
                      ],
                      "properties": {
                        "subscriber": {
                          "$ref": "#/components/schemas/SubscriberSummary"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Attributes are SDK-asserted and untrusted: any client holding the project's public key can set any custom key, so audience rules matching them are self-selectable. Never gate monetary value on flags/funnels alone — re-check entitlements server-side at purchase/redemption.",
        "parameters": [
          {
            "name": "X-Rovenue-App-User-Id",
            "in": "header",
            "required": true,
            "description": "Device key (the SDK's permanent `rovenueId`). `appUserContext` resolves or creates the subscriber from it; absent, the request is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-Platform",
            "in": "header",
            "required": false,
            "description": "First-install platform (`ios`, `android`, `web`). Persisted only when this call creates the subscriber; ignored otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "ios",
                "android",
                "web"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "attributes": {
                    "type": "object",
                    "additionalProperties": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "required": [
                  "attributes"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/me/entitlements": {
      "get": {
        "operationId": "getV1MeEntitlements",
        "summary": "List the calling subscriber's entitlements",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Entitlements.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "entitlements"
                      ],
                      "properties": {
                        "entitlements": {
                          "description": "Entitlement records granted to the subscriber. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "X-Rovenue-App-User-Id",
            "in": "header",
            "required": true,
            "description": "Device key (the SDK's permanent `rovenueId`). `appUserContext` resolves or creates the subscriber from it; absent, the request is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-Platform",
            "in": "header",
            "required": false,
            "description": "First-install platform (`ios`, `android`, `web`). Persisted only when this call creates the subscriber; ignored otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "ios",
                "android",
                "web"
              ]
            }
          }
        ]
      }
    },
    "/v1/offerings": {
      "get": {
        "operationId": "getV1Offerings",
        "summary": "List offerings with hydrated products",
        "tags": [
          "Commerce"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The project's offerings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "offerings"
                      ],
                      "properties": {
                        "offerings": {
                          "type": "array",
                          "items": {
                            "description": "Offering. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "An OFFERING-type experiment can override which offering is marked current when the caller identifies a subscriber.",
        "parameters": [
          {
            "name": "subscriberId",
            "in": "query",
            "required": false,
            "description": "SDK wire identity of the subscriber. Required unless supplied via the `X-Rovenue-User-Id` header; a request carrying neither is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-User-Id",
            "in": "header",
            "required": false,
            "description": "SDK wire identity of the subscriber. Fallback for the `subscriberId` query parameter; the query parameter wins when both are present.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/offerings/{identifier}": {
      "get": {
        "operationId": "getV1OfferingsByIdentifier",
        "summary": "Fetch one offering by identifier",
        "tags": [
          "Commerce"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The offering. `packages` is empty when no product could be hydrated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "identifier",
                        "isDefault",
                        "packages",
                        "metadata"
                      ],
                      "properties": {
                        "identifier": {
                          "type": "string"
                        },
                        "isDefault": {
                          "type": "boolean"
                        },
                        "packages": {
                          "type": "array",
                          "items": {
                            "description": "Package entry. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                          }
                        },
                        "metadata": {
                          "description": "Offering metadata. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No offering with that identifier in this project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "identifier",
            "in": "path",
            "required": true,
            "description": "Offering identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subscriberId",
            "in": "query",
            "required": false,
            "description": "SDK wire identity of the subscriber. Required unless supplied via the `X-Rovenue-User-Id` header; a request carrying neither is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-User-Id",
            "in": "header",
            "required": false,
            "description": "SDK wire identity of the subscriber. Fallback for the `subscriberId` query parameter; the query parameter wins when both are present.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/placements/{identifier}": {
      "get": {
        "operationId": "getV1PlacementsByIdentifier",
        "summary": "Resolve a placement to a funnel or funnel experiment",
        "tags": [
          "Placements"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The resolved placement. All three fields are null for an unknown or unmatched placement.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "placement",
                        "funnel",
                        "experiment"
                      ],
                      "properties": {
                        "placement": {
                          "description": "The matched placement, or null. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        },
                        "funnel": {
                          "description": "The published funnel snapshot, or null. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        },
                        "experiment": {
                          "description": "The funnel experiment envelope, or null. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Walks the placement's ordered audience rows to a native funnel or a `type=FUNNEL` experiment testing funnels. An unknown identifier returns an empty envelope with 200 — never a 404 — so a placement removed server-side degrades to 'show nothing' rather than an SDK error.",
        "parameters": [
          {
            "name": "identifier",
            "in": "path",
            "required": true,
            "description": "Placement identifier.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "BCP-47 tag. Text resolves by LANGUAGE: the exact tag, then shorter prefixes, then the funnel's default — case-insensitively.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subscriberId",
            "in": "query",
            "required": false,
            "description": "SDK wire identity of the subscriber. Required unless supplied via the `X-Rovenue-User-Id` header; a request carrying neither is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-User-Id",
            "in": "header",
            "required": false,
            "description": "SDK wire identity of the subscriber. Fallback for the `subscriberId` query parameter; the query parameter wins when both are present.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/purchases/apple-offer-signature": {
      "post": {
        "operationId": "postV1PurchasesAppleoffersignature",
        "summary": "Sign an Apple promotional offer",
        "tags": [
          "Commerce"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Signature material for `SKPaymentDiscount` / StoreKit 2.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "keyIdentifier",
                        "nonce",
                        "signature",
                        "timestamp"
                      ],
                      "properties": {
                        "keyIdentifier": {
                          "type": "string"
                        },
                        "nonce": {
                          "type": "string"
                        },
                        "signature": {
                          "type": "string"
                        },
                        "timestamp": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, `apple_offer_signing_unavailable` (no configured credentials), or `apple_offer_signing_failed`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Signs with the project's In-App Purchase key (which is NOT the App Store Connect API key).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "productId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "offerId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "appAccountToken": {
                    "type": "string"
                  }
                },
                "required": [
                  "productId",
                  "offerId"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/receipts/apple": {
      "post": {
        "operationId": "postV1ReceiptsApple",
        "summary": "Submit an Apple App Store receipt",
        "tags": [
          "Commerce"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Receipt accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "description": "Normalised purchase/subscription state derived from the receipt. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was reused with a different request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "The JWS chain is verified against the pinned Apple Root CAs; the verifier fails closed when no trust anchors are configured (`APPLE_ROOT_CERTS_DIR` unset or empty), in every environment.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opaque client-chosen key (≤255 chars). The first 2xx response is cached for 24h per project and replayed for repeats; a repeat carrying a different body is rejected with 422. Redis failures fail open, so dedup is best-effort.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "receipt": {
                    "type": "string",
                    "minLength": 1
                  },
                  "appUserId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "productId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "presentedContext": {
                    "type": "object",
                    "properties": {
                      "placementId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "funnelId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "variantId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "experimentKey": {
                        "type": "string",
                        "minLength": 1
                      }
                    },
                    "required": [
                      "placementId",
                      "funnelId"
                    ],
                    "additionalProperties": true
                  }
                },
                "required": [
                  "receipt",
                  "appUserId",
                  "productId"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/receipts/google": {
      "post": {
        "operationId": "postV1ReceiptsGoogle",
        "summary": "Submit a Google Play purchase token",
        "tags": [
          "Commerce"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Receipt accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "description": "Normalised purchase/subscription state derived from the purchase. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was reused with a different request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opaque client-chosen key (≤255 chars). The first 2xx response is cached for 24h per project and replayed for repeats; a repeat carrying a different body is rejected with 422. Redis failures fail open, so dedup is best-effort.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "receipt": {
                    "type": "string",
                    "minLength": 1
                  },
                  "appUserId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "productId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "presentedContext": {
                    "type": "object",
                    "properties": {
                      "placementId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "funnelId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "variantId": {
                        "type": "string",
                        "minLength": 1
                      },
                      "experimentKey": {
                        "type": "string",
                        "minLength": 1
                      }
                    },
                    "required": [
                      "placementId",
                      "funnelId"
                    ],
                    "additionalProperties": true
                  }
                },
                "required": [
                  "receipt",
                  "appUserId",
                  "productId"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/sdk/claim-install": {
      "post": {
        "operationId": "postV1SdkClaiminstall",
        "summary": "Look up a funnel claim token for a fresh install",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "A claim token was found.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "token"
                      ],
                      "properties": {
                        "token": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "404": {
            "description": "No claimable install matched. Returns `{ \"data\": null }` — the success envelope with a null payload, NOT the error envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "null"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "platform": {
                    "type": "string",
                    "enum": [
                      "ios",
                      "android"
                    ]
                  },
                  "locale": {
                    "type": "string",
                    "maxLength": 16
                  },
                  "timezone": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "screen_dims": {
                    "type": "string",
                    "maxLength": 16
                  },
                  "device_model": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "install_referrer": {
                    "type": "string",
                    "maxLength": 2048
                  },
                  "install_id": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128
                  }
                },
                "required": [
                  "platform",
                  "install_id"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/sdk/claim-via-email": {
      "post": {
        "operationId": "postV1SdkClaimviaemail",
        "summary": "Request a funnel claim by email address",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted. Returns `{ \"data\": null }` regardless of whether an address matched.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "null"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Always 202 with a null payload, whether or not the address matched anything — so the endpoint cannot be used to test whether an email address is a customer.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "maxLength": 254
                  },
                  "install_id": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128
                  }
                },
                "required": [
                  "email",
                  "install_id"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/sdk/sessions": {
      "post": {
        "operationId": "postV1SdkSessions",
        "summary": "Ingest a batch of SDK session events",
        "tags": [
          "Telemetry"
        ],
        "security": [
          {
            "publicApiKey": []
          }
        ],
        "responses": {
          "202": {
            "description": "Accepted. Empty body."
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The presented key is the wrong kind for this endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "503": {
            "description": "`TELEMETRY_UNAVAILABLE` — Kafka is unreachable or unconfigured. Deliberately not a 2xx, so the SDK keeps the buffered batch and retries instead of dropping it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Produced straight to Kafka with a content-derived, therefore stable, `eventId` — the SDK dispatcher is at-least-once, so a replayed batch must collapse rather than duplicate.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subscriberId": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 128
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "open",
                            "background",
                            "close"
                          ]
                        },
                        "occurredAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "durationMs": {
                          "anyOf": [
                            {
                              "type": "integer",
                              "minimum": 0
                            },
                            {
                              "type": "null"
                            }
                          ]
                        },
                        "appVersion": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 32
                        },
                        "sdkVersion": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 32
                        }
                      },
                      "required": [
                        "type",
                        "occurredAt",
                        "appVersion",
                        "sdkVersion"
                      ],
                      "additionalProperties": true
                    },
                    "minItems": 1,
                    "maxItems": 200
                  }
                },
                "required": [
                  "subscriberId",
                  "events"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/subscribers/{appUserId}/access": {
      "get": {
        "operationId": "getV1SubscribersByAppUserIdAccess",
        "summary": "Fetch a subscriber's access map by wire identity",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Entitlement access map.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "access"
                      ],
                      "properties": {
                        "access": {
                          "description": "Denormalised entitlement access map, keyed by entitlement identifier. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "appUserId",
            "in": "path",
            "required": true,
            "description": "SDK wire identity of the subscriber.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/subscribers/{appUserId}/attributes": {
      "post": {
        "operationId": "postV1SubscribersByAppUserIdAttributes",
        "summary": "Merge custom attributes onto a subscriber by wire identity",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The subscriber with merged attributes.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "subscriber"
                      ],
                      "properties": {
                        "subscriber": {
                          "$ref": "#/components/schemas/SubscriberSummary"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Attributes are SDK-asserted and untrusted: any client holding the project's public key can set any custom key, so audience rules matching them are self-selectable. Never gate monetary value on flags/funnels alone — re-check entitlements server-side at purchase/redemption.",
        "parameters": [
          {
            "name": "appUserId",
            "in": "path",
            "required": true,
            "description": "SDK wire identity of the subscriber.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "attributes": {
                    "type": "object",
                    "additionalProperties": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "required": [
                  "attributes"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/subscribers/{appUserId}/restore": {
      "post": {
        "operationId": "postV1SubscribersByAppUserIdRestore",
        "summary": "Restore purchases for a subscriber",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Restored purchases and the resulting access map.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "access",
                        "restored"
                      ],
                      "properties": {
                        "access": {
                          "description": "Denormalised entitlement access map, keyed by entitlement identifier. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        },
                        "restored": {
                          "description": "Purchases recovered by this call. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Re-validates the supplied store receipts and re-derives entitlements.",
        "parameters": [
          {
            "name": "appUserId",
            "in": "path",
            "required": true,
            "description": "SDK wire identity of the subscriber.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "receipts": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "store": {
                          "type": "string",
                          "enum": [
                            "APP_STORE",
                            "PLAY_STORE"
                          ]
                        },
                        "receipt": {
                          "type": "string",
                          "minLength": 1
                        },
                        "productId": {
                          "type": "string",
                          "minLength": 1
                        }
                      },
                      "required": [
                        "store",
                        "receipt",
                        "productId"
                      ],
                      "additionalProperties": true
                    }
                  }
                },
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/subscribers/claim-funnel-token": {
      "post": {
        "operationId": "postV1SubscribersClaimfunneltoken",
        "summary": "Claim a web-funnel purchase onto an app subscriber",
        "tags": [
          "Funnels"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Claim processed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "description": "Claim outcome. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "minLength": 40,
                    "maxLength": 64
                  },
                  "anon_id": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  }
                },
                "required": [
                  "token",
                  "anon_id"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/subscribers/transfer": {
      "post": {
        "operationId": "postV1SubscribersTransfer",
        "summary": "Merge one subscriber into another",
        "tags": [
          "Subscribers"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The merge outcome.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "description": "Merge result. Shape is not machine-derived — see the note at the top of openapi/responses.ts."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The presented key is the wrong kind for this endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "The server-side half of the SDK's `identify()`. `identify()` itself is client-local — there is no public-key alias endpoint — so an actual merge must come through here with a SECRET key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "fromAppUserId": {
                    "type": "string",
                    "minLength": 1
                  },
                  "toAppUserId": {
                    "type": "string",
                    "minLength": 1
                  }
                },
                "required": [
                  "fromAppUserId",
                  "toAppUserId"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/virtual-currencies/{appUserId}": {
      "get": {
        "operationId": "getV1VirtualcurrenciesByAppUserId",
        "summary": "Fetch a subscriber's virtual-currency balances",
        "tags": [
          "Virtual currencies"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Balances.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "balances"
                      ],
                      "properties": {
                        "balances": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Balance per currency code."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The presented key is the wrong kind for this endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "appUserId",
            "in": "path",
            "required": true,
            "description": "SDK wire identity of the subscriber.",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/virtual-currencies/{appUserId}/{code}/transactions": {
      "post": {
        "operationId": "postV1VirtualcurrenciesByAppUserIdByCodeTransactions",
        "summary": "Grant or spend virtual currency",
        "tags": [
          "Virtual currencies"
        ],
        "security": [
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The new balance.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "code",
                        "balance"
                      ],
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "balance": {
                          "type": "integer",
                          "description": "Balance after the transaction."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "403": {
            "description": "The presented key is the wrong kind for this endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "422": {
            "description": "The `Idempotency-Key` was reused with a different request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "Appends to `credit_ledger`, which is append-only and enforced as such by a database trigger. Spending is server-side only — the SDK has no client-side spend path.",
        "parameters": [
          {
            "name": "appUserId",
            "in": "path",
            "required": true,
            "description": "SDK wire identity of the subscriber.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "code",
            "in": "path",
            "required": true,
            "description": "Virtual-currency code.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Opaque client-chosen key (≤255 chars). The first 2xx response is cached for 24h per project and replayed for repeats; a repeat carrying a different body is rejected with 422. Redis failures fail open, so dedup is best-effort.",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "amount": {
                    "type": "integer",
                    "exclusiveMinimum": 0
                  },
                  "referenceId": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "referenceType": {
                    "type": "string",
                    "maxLength": 60
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 200
                  }
                },
                "required": [
                  "amount",
                  "referenceId"
                ],
                "additionalProperties": true
              }
            }
          }
        }
      }
    },
    "/v1/virtual-currencies/me": {
      "get": {
        "operationId": "getV1VirtualcurrenciesMe",
        "summary": "Fetch the calling subscriber's virtual-currency balances",
        "tags": [
          "Virtual currencies"
        ],
        "security": [
          {
            "publicApiKey": []
          },
          {
            "secretApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Balances.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "balances"
                      ],
                      "properties": {
                        "balances": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Balance per currency code."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Request body failed Zod validation (`VALIDATION_ERROR`), or a required identity/parameter was absent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "401": {
            "description": "Missing, malformed, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded — the global per-IP limiter or the 500 req/min per-API-key envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "description": "Unhandled server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "X-Rovenue-App-User-Id",
            "in": "header",
            "required": true,
            "description": "Device key (the SDK's permanent `rovenueId`). `appUserContext` resolves or creates the subscriber from it; absent, the request is rejected with 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "X-Rovenue-Platform",
            "in": "header",
            "required": false,
            "description": "First-install platform (`ios`, `android`, `web`). Persisted only when this call creates the subscriber; ignored otherwise.",
            "schema": {
              "type": "string",
              "enum": [
                "ios",
                "android",
                "web"
              ]
            }
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "publicApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A publishable project key (`rov_pub_…`), safe to ship inside a client application."
      },
      "secretApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A secret project key (`rov_sec_…`). Server-to-server only — it must never reach a client."
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "error"
        ],
        "additionalProperties": false,
        "description": "Every non-2xx response in the API uses this shape. `code` is a stable machine-readable identifier; `message` is human-facing and may change.",
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "additionalProperties": false,
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "SubscriberSummary": {
        "type": "object",
        "required": [
          "id",
          "appUserId",
          "attributes"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Rovenue-owned subscriber id (cuid2)."
          },
          "appUserId": {
            "type": "string",
            "description": "The SDK wire identity (`rovenueId`) for this subscriber."
          },
          "attributes": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "string",
                "null"
              ]
            },
            "description": "Flattened custom attributes."
          }
        }
      }
    }
  },
  "x-rovenue-browser-surface": {
    "description": "The `/v1` router is mounted twice. Browser callers use the second mount, whose path carries the publishable key because a CORS preflight (an OPTIONS request with no `Authorization` header) has no other way to identify the project whose origin allow-list should answer it. Every path in `paths` is reachable under this prefix too, with identical request bodies, responses and key requirements — that is why they are not listed twice.",
    "pathPrefix": "/v1/web/{publicKey}",
    "publicKeyParameter": {
      "name": "publicKey",
      "in": "path",
      "required": true,
      "description": "The publishable key (`rov_pub_…`) of the calling project. Must be the SAME key presented in the `Authorization` header: `requireMatchingPathKey` rejects the pair otherwise, so a site cannot combine its own allow-listed path key with another project's Bearer key and call from an origin that project never permitted.",
      "schema": {
        "type": "string"
      }
    },
    "cors": "Per-key: the allowed origins are the calling key's `allowedOrigins`. An unknown or revoked key yields an empty list, which refuses every origin — indistinguishable from a real key that was never enabled for browser use. `Access-Control-Allow-Credentials` is deliberately NOT set on this surface, since it would land on top of a reflected customer origin.",
    "rateLimit": "The unauthenticated preflight lookup sits behind the global per-IP limiter, which is registered before the per-key CORS middleware for exactly that reason."
  },
  "x-rovenue-generation": {
    "generatedFrom": "apps/api/scripts/generate-openapi.ts, walking app.routes",
    "derived": [
      "endpoint set",
      "request bodies",
      "security requirements"
    ],
    "handMaintained": [
      "responses",
      "parameters",
      "summaries and descriptions",
      "tags"
    ],
    "handMaintainedIn": "apps/api/openapi/responses.ts"
  }
}
