{"openapi":"3.0.3","info":{"version":"0.25.1","title":"Axeptio Headless CMP API","description":"Comprehensive Consent Management Platform API for headless implementations.\n\n## API Overview\n\nThis API provides complete CMP functionality including:\n- **Authentication & Authorization**: Secure access control and user management\n- **Project Management**: Create and configure consent collection projects\n- **Consent Management**: Collect, store, and retrieve user consent decisions\n- **Cookie & Vendor Configuration**: Define consent categories and third-party services\n- **Analytics & Reporting**: Access consent statistics and compliance reports\n- **Organization Management**: Multi-tenant support for agencies and enterprises\n\n## Authentication\n\nAll endpoints require Bearer token authentication:\n```\nAuthorization: Bearer YOUR_API_TOKEN\n```\n\n## Rate Limits\n\nRate limits vary by customer tier:\n- **Free**: 100 requests per minute\n- **Pro**: 1000 requests per minute\n- **Enterprise**: 10000 requests per minute\n\n## Platform Support\n\nThis API supports all platforms and frameworks including web, mobile (iOS, Android, React Native, Flutter), and server-side implementations."},"tags":[{"name":"Consent Ingestion","description":"High-performance consent collection endpoints optimized  with offline sync capabilities and batch operations.","externalDocs":{"description":"Mobile Consent Collection Guide","url":"https://docs.axeptio.tech/mobile/consent-collection"}},{"name":"Consent Reading","description":"consent retrieval with compact response formats and platform-specific optimizations for fast mobile loading.","externalDocs":{"description":"Mobile Consent Reading Guide","url":"https://docs.axeptio.tech/mobile/consent-reading"}},{"name":"Batch Operations","description":"Bulk consent operations designed for mobile offline sync scenarios, allowing submission of multiple consent records in a single request.","externalDocs":{"description":"Mobile Offline Sync Guide","url":"https://docs.axeptio.tech/mobile/offline-sync"}},{"name":"Health Check","description":"service health monitoring with circuit breaker status and disaster recovery information for application reliability.","externalDocs":{"description":"Mobile Monitoring Guide","url":"https://docs.axeptio.tech/mobile/monitoring"}},{"name":"CORS","description":"Cross-Origin Resource Sharing support for mobile web views, hybrid apps, and browser-based mobile implementations.","externalDocs":{"description":"Mobile CORS Setup","url":"https://docs.axeptio.tech/mobile/cors"}},{"name":"SDK","description":"endpoints designed for native iOS, Android, React Native, and Flutter applications","externalDocs":{"description":"Mobile Integration Guide","url":"https://axeptio.tech/mobile-integration"}},{"name":"Public","description":"Unauthenticated endpoints, safe to call directly from a visitor's browser or a tag manager. They compute views over data the widget already fetches publicly, so they disclose nothing a caller could not obtain from the public CDN. Everything else on this API requires a bearer token."},{"name":"TCF","description":"IAB TCF v2.x helpers: GVL snapshots served from KV (refreshed daily by a scheduled task) and a stateless TC-string decoder. Same bearer-token auth and per-project rate limiter (shared across the project's tokens) as the rest of /mobile/*.","externalDocs":{"description":"IAB TCF v2.2 specification","url":"https://iabeurope.eu/tcf/"}}],"paths":{"/mobile/consents/{clientId}/{collection}/{configId}":{"post":{"tags":["Consent Ingestion"],"summary":"Submit consent record for mobile applications","description":"Records user consent preferences with mobile-specific optimizations including offline sync support and platform-specific metadata","operationId":"mobile_submitMobileConsent","security":[{"bearerAuth":[]}],"parameters":[{"name":"clientId","in":"path","required":true,"description":"Project/Client identifier (24-character hex string)","schema":{"type":"string","pattern":"^[a-f\\d]{24}$","example":"507f1f77bcf86cd799439011"}},{"name":"collection","in":"path","required":true,"description":"Type of consent being collected. `terms` is the public alias for `contractsV2` (Terms & Conditions).","schema":{"type":"string","enum":["processings","contracts","contractsV2","cookies","terms"],"example":"processings"}},{"name":"configId","in":"path","required":true,"description":"Configuration identifier, can include consentFor suffix for contractsV2","schema":{"type":"string","maxLength":255,"example":"507f1f77bcf86cd799439012"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentSubmissionRequest"},"examples":{"ios-consent":{"summary":"iOS app consent submission","value":{"accept":true,"token":"mobile_user_12345","timestamp":"2025-09-03T10:30:00Z","headers":{"x-mobile-platform":"ios","x-mobile-version":"17.0","x-app-version":"2.1.0","x-device-id":"uuid-device-123","user-agent":"MyApp/2.1.0 (iOS 17.0; iPhone)"},"preferences":{"vendors":{"google":true,"facebook":false,"analytics":true},"config":{"name":"Cookie Consent","language":"en","consentMode":"opt-in","mobileContext":{"platform":"ios","offline":false,"networkType":"wifi"}},"googleConsentMode":{"version":2,"analytics_storage":"granted","ad_storage":"denied"}}}},"android-batch":{"summary":"Android offline sync batch","value":{"accept":false,"token":"android_user_67890","timestamp":"2025-09-03T09:15:00Z","headers":{"x-mobile-platform":"android","x-mobile-version":"14","x-app-version":"3.2.1","x-device-id":"android-uuid-456","x-session-id":"session-789"},"preferences":{"config":{"mobileContext":{"platform":"android","offline":true,"syncBatch":"batch-002","networkType":"cellular"}}}}},"react-native":{"summary":"React Native cross-platform consent","value":{"accept":true,"token":"rn_user_11111","headers":{"x-mobile-platform":"react-native","x-app-version":"1.5.2"},"preferences":{"vendors":{"amplitude":true,"mixpanel":true},"config":{"mobileContext":{"platform":"react-native","offline":false}}}}}}}}},"responses":{"200":{"description":"Consent successfully recorded","headers":{"X-DB-Mode":{"description":"Database mode used (normal/pool)","schema":{"type":"string"}},"X-Circuit-Breaker":{"description":"Circuit breaker status","schema":{"type":"string","enum":["enabled","disabled"]}},"X-S3-Fallback":{"description":"S3 fallback status","schema":{"type":"string","enum":["enabled","disabled"]}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentSubmissionResponse"}}}},"400":{"description":"Invalid request data or validation error.\n\nLegacy validation/parse failures are returned as `text/plain`. JSON `ErrorResponse` bodies are returned for:\n- TCF submissions (bodies containing `preferences.tcString`) — `invalid_argument` (body failed JSON-Schema validation against `tcf-consent.schema.json`; `details[]` carries the per-field errors), `bad_format` (`tcString` is not a parseable TCF v2 base64url string), `unsupported_version` (TCF v1 or future major version the decoder does not implement), `invalid_cmp` (decoded `cmpId === 0` or `cmpVersion === 0` — TC string from an unregistered/stub CMP), `unknown_gvl_version` (decoded `vendorListVersion` is not present in the TCF_GVL KV namespace and is not satisfied by a refresh of `gvl:latest`; when the cached pointer is present, the refresh is gated to a single lookahead step beyond it, otherwise — pointer missing or unreadable — the refresh runs unconditionally so a freshly-deployed worker can still accept a legitimate version).\n- Any submission when the project has `defaultConfigIdEnforcement: \"enforce\"` and the URL `configId` does not match `headlessDefaultConfigId` — `invalid_configuration` (rejected by `validateConsentSubmission`; emitted regardless of whether the body carries a `tcString`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"tcf-invalid-argument":{"summary":"TCF body failed schema validation","value":{"error":"invalid_argument","message":"Invalid TCF consent payload","timestamp":"2026-05-13T10:30:00.000Z","details":[{"field":"preferences.tcString","message":"must be string","constraint":"type"}]}},"tcf-bad-format":{"summary":"TC string is not parseable","value":{"error":"bad_format","message":"Invalid TCF consent string","timestamp":"2026-05-13T10:30:00.000Z"}},"tcf-unsupported-version":{"summary":"TCF v1 / future major version","value":{"error":"unsupported_version","message":"Unsupported TCF version","timestamp":"2026-05-13T10:30:00.000Z"}},"tcf-invalid-cmp":{"summary":"Unregistered CMP id or version","value":{"error":"invalid_cmp","message":"TC string has unregistered cmpId or cmpVersion","timestamp":"2026-05-13T10:30:00.000Z"}},"tcf-unknown-gvl-version":{"summary":"vendorListVersion not in TCF_GVL KV","value":{"error":"unknown_gvl_version","message":"TC string references an unknown GVL version","timestamp":"2026-05-13T10:30:00.000Z"}},"invalid-configuration":{"summary":"defaultConfigId enforcement rejected the configId","value":{"error":"invalid_configuration","message":"Consent configuration mismatch","timestamp":"2026-05-13T10:30:00.000Z"}}}},"text/plain":{"examples":{"validation-error":{"summary":"Non-TCF validation error","value":"Validation error: /accept must be boolean [requestId: abc123]"},"parse-error":{"summary":"Parameter parsing error","value":"Error while parsing path parameters [requestId: def456]"}}}}},"413":{"description":"Request body exceeds the 100 KB cap (`MAX_BODY_BYTES = 102400`). Two body-reading paths in `worker.js` enforce the cap on this route and can each return `payload_too_large`:\n- TCF validation read — runs for all `application/json` submissions (independently of whether the body carries `preferences.tcString`) and is gated by a Content-Length pre-filter + streaming `readBodyWithCap` cutoff.\n- defaultConfigId override read — when `validateConsentSubmission` returns the `override` action on a POST (the only method routed through the gateway's consent-validation block), the worker streams the body via `readBodyWithCap` to rebuild the request with the overridden path. This branch reads the body *before* any Content-Type check, so non-JSON submissions can also hit the cap when the project has an override-mode enforcement configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"payload-too-large":{"value":{"error":"payload_too_large","message":"Request body must not exceed 102400 bytes","timestamp":"2026-05-13T10:30:00.000Z"}}}}}},"500":{"description":"Internal server error. Returned as `text/plain` for downstream (DB / Kinesis / fallback) failures, and as a JSON `ErrorResponse` for gateway-level failures on this route. JSON `error` codes observed here include `internal_error` (TCF body read or GVL lookup against TCF_GVL KV failed), `configuration_error` (required backend URL env var missing, OR project authorization data in `AUTHORIZATION_KV` is corrupted / unparseable — `validateConsentSubmission` fails closed), and `internal_server_error` (top-level catch in the API-request handler).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"tcf-body-read-failure":{"summary":"Body unreadable for TCF detection","value":{"error":"internal_error","message":"Failed to read consent request body for validation","timestamp":"2026-05-13T10:30:00.000Z"}},"tcf-gvl-lookup-failure":{"summary":"TCF_GVL KV lookup failed","value":{"error":"internal_error","message":"GVL lookup failed","timestamp":"2026-05-13T10:30:00.000Z"}},"configuration-error-env":{"summary":"Backend URL env var missing","value":{"error":"configuration_error","message":"Backend API URL is not configured. Please contact support.","timestamp":"2026-05-13T10:30:00.000Z"}},"configuration-error-project":{"summary":"Project authorization data is corrupted / unparseable","value":{"error":"configuration_error","message":"Project configuration data error","timestamp":"2026-05-13T10:30:00.000Z"}},"internal-server-error":{"summary":"Unhandled API-request failure","value":{"error":"internal_server_error","message":"Failed to process API request","timestamp":"2026-05-13T10:30:00.000Z"}}}},"text/plain":{"examples":{"db-error":{"summary":"Database error","value":"Error while inserting consent in DB [requestId: ghi789]"},"stream-error":{"summary":"Kinesis stream error","value":"Error while sending consent to stream [requestId: jkl012]"},"fallback-error":{"summary":"Both DB and S3 fallback failed","value":"Error while inserting consent in DB and S3 fallback [requestId: mno345]"}}}}},"503":{"description":"Storage service temporarily unavailable. Emitted by `validateConsentSubmission` when the `AUTHORIZATION_KV` read for project authorization throws — the gateway fails closed rather than waving the request through, so clients should treat this as transient and retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"examples":{"storage-unavailable":{"value":{"error":"storage_unavailable","message":"Storage service temporarily unavailable","timestamp":"2026-05-13T10:30:00.000Z"}}}}}}}},"options":{"tags":["CORS"],"summary":"CORS preflight for consent ingestion","description":"Handles CORS preflight requests for mobile consent submission","responses":{"200":{"description":"CORS preflight response","headers":{"Access-Control-Allow-Origin":{"schema":{"type":"string"}},"Access-Control-Allow-Methods":{"schema":{"type":"string"}},"Access-Control-Allow-Headers":{"schema":{"type":"string"}}}}}}},"/mobile/health":{"get":{"tags":["Health Check"],"summary":"Mobile service health check","description":"Health check endpoint optimized for mobile monitoring and circuit breaker status","responses":{"200":{"description":"Service healthy","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthCheckResponse"}}}}}}},"/mobile/changelog":{"get":{"tags":["Mobile SDK"],"summary":"API changelog","description":"Curated, API-behavior-level changelog for SDK integrators — breaking/behavior changes, new features, and deprecations per endpoint, newest first. Distinct from the commit-level repository CHANGELOG. Every `/mobile/*` response also carries an `X-Api-Changelog: /mobile/changelog` header; a path scheduled for removal additionally carries RFC 8594 `Deprecation: true` and `Sunset` headers.","operationId":"mobile_getMobileChangelog","responses":{"200":{"description":"The API changelog (newest first)","headers":{"X-Api-Changelog":{"description":"Path to this changelog; present on every /mobile/* response.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MobileChangelog"},"examples":{"changelog":{"summary":"Recent API changes","value":{"entries":[{"id":"eng-12029-categories-scope","date":"2026-07-07","version":"0.18.0","endpoint":"/mobile/vendors/{projectId}/categories","type":"breaking","summary":"The project-wide categories endpoint now returns a single selected configuration's purpose-step categories instead of the union across all configurations, and `name` is now a localized display label.","migration":"Use the new `category` field (canonical slug) as the stable key and `title` for the display label. The flat vendor list still returns the full union. Pass `?lang=` to select a language."}]}}}}}}}}},"/mobile/configurations/{projectId}":{"get":{"tags":["Mobile SDK"],"summary":"Get mobile-optimized project configuration","description":"Retrieve project configuration optimized for mobile apps with minimal payload size and mobile-specific settings.","operationId":"mobile_getMobileConfig","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"},"description":"Project identifier","example":"60f7b3b3d1b6c4a7c8e9f0a1"},{"name":"platform","in":"query","required":false,"schema":{"type":"string","enum":["ios","android","react-native","flutter"]},"description":"Mobile platform for optimized responses","example":"ios"},{"name":"version","in":"query","required":false,"schema":{"type":"string"},"description":"App version for feature flag support","example":"1.2.3"},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en"},"description":"Language code (typically the two-letter lowercase code derived from the device locale, e.g. `en`, `fr`) for the localized `screens` content. Full locale tags (e.g. `fr-CA`) are also accepted: they resolve to the exact regional variant when one is bundled, otherwise to their primary subtag; matching is case-insensitive. Missing or unrecognized values silently fall back to English — never an error. Only affects `screens`; `configurations[]` entries are already per-language.","example":"fr"}],"responses":{"200":{"description":"Mobile project configuration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MobileProjectConfig"},"examples":{"merged-config":{"summary":"Merged Brands + TCF configuration","value":{"projectId":"67fcdb2b52ab9a99a5865f4d","primaryColor":"#013974","isDomainRestrictionEnabled":false,"verifiedDomains":[],"configurations":[{"identifier":"6859079473219bcbb8435079","name":"Cookies","language":"en","country":"Unknown","title":"Cookies","flowType":"brands","isDefault":true,"askNewConsent":false,"googleConsentMode":{"display":true,"displayIllustrations":true,"position":"second","adsDataRedaction":false,"urlPassthrough":false},"policyUrl":"https://example.com/privacy","cookieStatementUrl":"https://example.com/cookies"},{"identifier":"60a1b2c3d4e5f60718293a4b","name":"TCF","language":"en","country":"Unknown","title":"TCF Consent","flowType":"tcf","isDefault":false,"cmpVersion":4,"googleAdditionalConsent":true,"googleConsentMode":{"adsDataRedaction":false,"urlPassthrough":false},"askNewConsent":true,"purposes":[],"stacks":[1,2,3,4,5,6,7,30,39,40],"expirationTtlDays":180,"policyUrl":"https://example.com/privacy","cookieStatementUrl":null}],"defaultConfigId":"6859079473219bcbb8435079","screens":{"att":{"background":["#E9CBFF","#FFCCCD"],"media":null,"title":"Your Tracking Choice, Your Control","description":"A pop-in will ask whether you'd like to allow or decline tracking. Your decision will update your previous cookie preferences. If you previously accepted cookies but choose to decline tracking, the related cookies will be deleted to respect your choice."},"permissions":{"background":["#FFF8CC","#CCF6FF"],"media":null,"title":"Unlock all features of the app","description":"Allow the app to use various features on your device. The app will send a notification to ask for permission to use features on your device, which you can Allow or Deny."}}}}}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/token":{"get":{"tags":["Mobile SDK"],"summary":"Generate consent token","description":"Generate a cryptographically secure random token for consent identification. This token is used to identify and link consent choices in the consent management system.","operationId":"mobile_generateConsentToken","responses":{"200":{"description":"Successfully generated consent token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentTokenResponse"},"examples":{"token-response":{"summary":"Generated consent token","value":{"token":"abc123def456ghi789"}}}}}},"401":{"description":"Unauthorized - Invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed - Only GET requests supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/auth/me":{"get":{"tags":["Mobile SDK"],"summary":"Validate bearer token and get project information","description":"Validates the provided bearer token and returns associated project information and tier details.","operationId":"mobile_validateBearerToken","responses":{"200":{"description":"Token validation successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthValidationResponse"},"examples":{"valid-token":{"summary":"Valid token response","value":{"valid":true,"projectId":"67fcdb2b52ab9a99a5865f4d","tier":"enterprise"}}}}}},"401":{"description":"Unauthorized - Invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"invalid-token":{"summary":"Invalid token","value":{"error":"unauthorized","message":"Invalid or missing authorization header"}}}}}},"405":{"description":"Method not allowed - Only GET requests supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/vendors/{projectId}":{"get":{"tags":["Mobile SDK"],"summary":"Get project vendors","description":"Retrieve all vendors for a project with mobile-optimized data including imgix-optimized images for different screen densities. The aggregate unions vendors from both the Brands (cookies[]) and TCF (consents[]) configurations, so vendors from pure-TCF configs are included (with `iabId` populated for IAB-registered vendors). The optional `lang` query parameter selects which single-language configuration's localized vendor strings (`title`/`description`) are surfaced; the vendor union is unchanged.","operationId":"mobile_getProjectVendors","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"},"description":"Project identifier","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"lang","in":"query","required":false,"schema":{"type":"string"},"description":"Language code (e.g. `en`, `fr`) selecting which single-language configuration's localized vendor strings win when the same vendor appears in several configurations. Full locale tags (e.g. `fr-CA`) resolve to the exact `language` match, else the primary subtag; case-insensitive. Missing or unrecognized values silently fall back to the project default configuration, else the first — never an error. The vendor union across configurations is preserved; only the surfaced strings change.","example":"fr"}],"responses":{"200":{"description":"Successfully retrieved project vendors","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VendorsResponse"},"examples":{"vendors-response":{"summary":"Project vendors with optimization","value":{"projectId":"67fcdb2b52ab9a99a5865f4d","vendors":[{"id":"62fd02d250c4539d3456e350","iabId":755,"name":"googletagmanager","title":"Google Tag Manager","description":"Système de gestion de balisage","longDescription":"Mise en place d'un système de balisage pour traiter plus facilement les informations liées aux données.","category":"Fonctionnels, Gestion des étiquettes","categories":["Fonctionnels","Gestion des étiquettes"],"type":"functional, tag_management","domain":"https://tagmanager.google.com/","policyUrl":"https://marketingplatform.google.com/about/analytics/tag-manager/use-policy/","isAxeptioVendor":true,"image":{"identifier":"googletagmanager","baseUrl":"https://axeptio.imgix.net/vendors/googletagmanager","optimized":{"small":"https://axeptio.imgix.net/vendors/googletagmanager?w=32&h=32&fit=crop&auto=format,compress","medium":"https://axeptio.imgix.net/vendors/googletagmanager?w=64&h=64&fit=crop&auto=format,compress","large":"https://axeptio.imgix.net/vendors/googletagmanager?w=128&h=128&fit=crop&auto=format,compress"},"fallbackUrl":"https://favicons.axept.io/favicons?domain=tagmanager.google.com"}}],"totalVendors":5,"categories":["Fonctionnels","Gestion des étiquettes","Réseaux sociaux"],"fetchedAt":"2025-09-12T14:30:00.000Z"}}}}}},"401":{"description":"Unauthorized - Invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Project not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"Bad gateway — upstream config service unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/vendors/{projectId}/categories":{"get":{"tags":["Mobile SDK"],"summary":"Get vendors by category","description":"Retrieve vendors organized by category for a project. Categories are sourced from a single configuration's purpose steps (Brands), so each carries a canonical `category` slug (the stable, language-independent key), a localized `name`/`title` label, plus `subtitle`, `description`, and an `image` URL when the customer set a custom illustration. On this endpoint `name` is the localized label (backward compatible with the historical free-text `name`); prefer `category` (slug) and `title` (label). The optional `lang` query parameter selects which single-language configuration answers (multilingual projects publish one configuration per language). The resolved `configId`/`language` are echoed. Projects without a Brands configuration (e.g. TCF-only) fall back to legacy free-text grouping (`category` null, `name`/`vendors`/`count` only).","operationId":"mobile_getVendorCategories","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"},"description":"Project identifier","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"lang","in":"query","required":false,"schema":{"type":"string"},"description":"Language code (typically the two-letter lowercase code from the device locale, e.g. `en`, `fr`) selecting which single-language configuration to serve. Full locale tags (e.g. `fr-CA`) are accepted: they resolve to the exact `language` match, else the primary subtag; matching is case-insensitive. Missing or unrecognized values silently fall back to the project default configuration, else the first — never an error. This SELECTS a configuration; it does not translate one (a configuration is single-language upstream).","example":"fr"}],"responses":{"200":{"description":"Successfully retrieved vendor categories","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VendorCategoriesResponse"},"examples":{"categories-response":{"summary":"Vendors grouped by category (step-sourced, ?lang=fr)","value":{"projectId":"67fcdb2b52ab9a99a5865f4d","configId":"67fcddc2673e895f81feb6c3","configName":"fr-eu-14-04-2025","language":"fr","welcomingScreen":{"title":"Bienvenue !","subtitle":"Votre vie privée compte","description":"Nous utilisons des cookies pour améliorer votre expérience.","picture":null},"categories":[{"category":"analytics","name":"Statistiques et audience","title":"Statistiques et audience","subtitle":"Compter nos calories, c'est important !","description":"Nous comptons le nombre de visites sur ce site.","image":null,"vendors":[{"id":"62fd02d250c4539d3456e350","iabId":755,"name":"googletagmanager","title":"Google Tag Manager"}],"count":3},{"category":"social_networks","name":"Réseaux sociaux","title":"Réseaux sociaux","subtitle":null,"description":null,"image":null,"vendors":[{"id":"6282644788ca8e0d930c0082","name":"twitter","title":"X"}],"count":2}],"totalCategories":4,"totalVendors":12,"fetchedAt":"2025-09-12T14:30:00.000Z"}}}}}},"401":{"description":"Unauthorized - Invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Project not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/vendors/{projectId}/{configId}":{"get":{"tags":["Mobile SDK"],"summary":"Get vendors for specific configuration","description":"Retrieve vendors for a specific project configuration. This provides more precise vendor lists for context-specific consent collection compared to the aggregated project vendors endpoint. Resolves both Brands and TCF configurations: the configId is matched against the Brands cookies[] and the TCF consents[] upstreams, so a TCF (Publisher) configId returns its vendor list (with `iabId` populated for IAB-registered vendors) instead of 404.","operationId":"mobile_getConfigurationVendors","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-fA-F]{24}$"},"description":"Project identifier (MongoDB ObjectId)","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"configId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-fA-F]{24}$"},"description":"Configuration identifier (MongoDB ObjectId) — may be a Brands or a TCF (Publisher) configuration","example":"67fcddc2673e895f81feb6c3"}],"responses":{"200":{"description":"Successfully retrieved configuration vendors","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigurationVendorsResponse"},"examples":{"config-vendors-response":{"summary":"Configuration-specific vendors","value":{"projectId":"67fcdb2b52ab9a99a5865f4d","configId":"67fcddc2673e895f81feb6c3","configName":"fr-eu-14-04-2025","vendors":[{"id":"62fd02d250c4539d3456e350","iabId":755,"name":"googletagmanager","title":"Google Tag Manager","description":"Système de gestion de balisage","longDescription":"Google Tag Manager facilite la mise à jour des balises de mesure d'audience et de remarketing sur vos sites Web et vos applications mobiles.","category":"Fonctionnels","categories":["Fonctionnels"],"type":"cookie","domain":"https://tagmanager.google.com/","policyUrl":"https://marketingplatform.google.com/about/analytics/tag-manager/use-policy/","isAxeptioVendor":true,"image":{"identifier":"googletagmanager","baseUrl":"https://axeptio.imgix.net/vendors/googletagmanager","optimized":{"small":"https://axeptio.imgix.net/vendors/googletagmanager?w=32&h=32&fit=crop&auto=format,compress","medium":"https://axeptio.imgix.net/vendors/googletagmanager?w=64&h=64&fit=crop&auto=format,compress","large":"https://axeptio.imgix.net/vendors/googletagmanager?w=128&h=128&fit=crop&auto=format,compress"},"fallbackUrl":"https://favicons.axept.io/favicons?domain=tagmanager.google.com"}}],"totalVendors":1,"categories":["Fonctionnels"],"fetchedAt":"2025-09-22T10:30:00.000Z"}}}}}},"400":{"description":"Invalid project ID or configuration ID format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"403":{"description":"Access denied to this project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Project or configuration not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"Bad gateway — upstream config service unreachable (e.g. the TCF upstream is down for a TCF configId).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/vendors/{projectId}/{configId}/categories":{"get":{"tags":["Mobile SDK"],"summary":"Get vendors by category for a specific configuration","description":"Retrieve vendors organized by category for a specific project configuration. The response includes a `welcomingScreen` field (title, subtitle, description, picture) sourced from the configuration's first non-vendor step for Brands configs, and partially from the consent-row root for TCF/Publisher configs (title, subtitle from `subtitle` falling back to `surtitle`, picture from `logo`; description a product-authored hardcoded string localized off the consent row's language with an English fallback); null for metadata-less configs. Categories are sourced from the configuration's purpose steps (Brands configs), so each category carries a canonical `category` slug (e.g. `analytics`, `targeted_ads`; on this endpoint `name` mirrors the slug for its existing contract) plus the localized `title`, `subtitle`, `description`, and an `image` URL when the customer set a custom illustration (null otherwise). Prefer `category` (slug) and `title` (label). Because a configuration is single-language, these fields are already localized — no language parameter is needed. Configurations without purpose steps (e.g. TCF) fall back to free-text category grouping with `category` null and the metadata fields set to null.","operationId":"mobile_getConfigurationVendorCategories","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-fA-F]{24}$"},"description":"Project identifier (MongoDB ObjectId)","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"configId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-fA-F]{24}$"},"description":"Configuration identifier (MongoDB ObjectId)","example":"67fcddc2673e895f81feb6c3"}],"responses":{"200":{"description":"Successfully retrieved vendor categories for the configuration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfigurationVendorCategoriesResponse"},"examples":{"config-categories-response":{"summary":"Configuration vendors grouped by category","value":{"projectId":"67fcdb2b52ab9a99a5865f4d","configId":"67fcddc2673e895f81feb6c3","configName":"fr-eu-14-04-2025","welcomingScreen":{"title":"Votre vie privée, nos engagements","subtitle":"Quelques instants pour choisir","description":"Nous utilisons des technologies pour vous offrir une meilleure expérience.","picture":"https://axeptio.imgix.net/2025/04/welcome-illustration.png"},"categories":[{"category":"analytics","name":"analytics","title":"Statistiques et audience","subtitle":"Compter nos calories, c'est important !","description":"Nous sommes chargés de compter le nombre de visites sur ce site.","image":"https://axeptio.imgix.net/2025/04/analytics-illustration.png","vendors":[{"id":"62fd02d250c4539d3456e350","iabId":755,"name":"googletagmanager","title":"Google Tag Manager"}],"count":1},{"category":"targeted_ads","name":"targeted_ads","title":"Publicité Ciblée","subtitle":"Du contenu fait spécialement pour vous !","description":"Afficher des publicités personnalisées.","image":null,"vendors":[],"count":0}],"totalCategories":2,"totalVendors":1,"fetchedAt":"2025-09-22T10:30:00.000Z"}}}}}},"400":{"description":"Bad request - Invalid project or configuration ID format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"401":{"description":"Unauthorized - Invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"403":{"description":"Forbidden - Token does not grant access to this project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Project or configuration not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"Bad gateway — upstream config service unreachable (e.g. the TCF upstream is down for a TCF configId).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/analytics/evts":{"post":{"tags":["Mobile SDK"],"summary":"Submit analytics events","description":"Submit analytics and behavioral events. When the `X-Mobile-Platform: headless` header is present, the gateway applies strict validation against the HeadlessEvent schema, enriches each event with the server-side `projectId` and `source`, and proxies the batch to the events backend.\n\n**Headless mode constraints:**\n- Request body must be a JSON **array** of event objects (not a single object)\n- Maximum payload size: **100 KB**\n- Maximum batch size: **100 events**\n- All events are validated atomically — if any event fails validation, the entire batch is rejected\n- Every event must include `type`, `timestamp`, `token`, `domain`, `config` (at least `identifier`), `preferences` (`enabled`/`disabled` arrays), `userAgent`, `pathname`, and `referrer` — see the HeadlessEvent schema below. `config`/`preferences`/`userAgent`/`pathname`/`referrer` are required by the events backend but are **not** synthesised by the gateway, so omitting them is rejected locally with a `validation_error` (see the `missing-backend-required-field` 400 example)\n- The `projectId` field is injected server-side from the bearer token context and cannot be set by the client\n- The `source` field is set server-side based on the `X-Device-OS` header and the project's OS detection configuration — do not send `X-Mobile-Platform: ios`/`android` expecting headless validation/enrichment; only the literal value `headless` triggers it\n\n**Recommended event sequence for a consent flow:**\n1. `page_view` — user lands on the page\n2. `consent_displayed` — consent UI is shown\n3. `consent_accepted` / `consent_rejected` / `consent_partial` — user makes a choice\n\nOptionally, if the user opens the settings panel: `settings_opened` → `category_toggled` (per toggle) → final consent event.","operationId":"mobile_submitMobileEvents","parameters":[{"name":"X-Mobile-Platform","in":"header","description":"Client platform identifier. Only the value `headless` triggers server-side schema validation and event enrichment. Other values (`ios`, `android`) or absence of this header cause events to be proxied directly to the backend without gateway validation.","required":false,"schema":{"type":"string","enum":["headless","ios","android"]},"example":"headless"},{"name":"X-Device-OS","in":"header","description":"Device operating system. Used for source attribution — enabled by default for every project. When set to `ios` or `android`, the event `source` is enriched as `sdk-ios` or `sdk-android` respectively; an absent or unrecognized value falls back to `sdk-headless`. A project can opt out entirely by setting `headlessOsDetection: false` in its project config, which forces `sdk-headless` regardless of this header.","required":false,"schema":{"type":"string","enum":["ios","android"]},"example":"ios"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"description":"Request body shape depends on the X-Mobile-Platform header. When X-Mobile-Platform: headless, send an array of HeadlessEvent. For native mobile clients (non-headless), the legacy MobileEvent shape is also accepted.","oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/HeadlessEvent"},"minItems":1,"maxItems":100,"description":"Headless mode: array of HeadlessEvent objects (1–100 items, max 100 KB total). Required when X-Mobile-Platform: headless."},{"$ref":"#/components/schemas/MobileEvent","description":"Legacy mobile mode: single MobileEvent object. Used by existing native SDKs when X-Mobile-Platform is not headless."},{"type":"array","items":{"$ref":"#/components/schemas/MobileEvent"},"minItems":1,"description":"Legacy mobile mode: array of MobileEvent objects. Used by existing native SDKs when X-Mobile-Platform is not headless."}]},"examples":{"consent-accepted-flow":{"summary":"Typical consent acceptance flow (3 events)","value":[{"type":"page_view","timestamp":"2025-09-18T10:30:00.000Z","token":"a3b7x9k2m5p8q1w4","domain":"example.com","config":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"},"preferences":{"enabled":[],"disabled":[]},"userAgent":"MyApp/2.1.0 (iOS 17.0; iPhone)","pathname":"/","referrer":null},{"type":"consent_displayed","timestamp":"2025-09-18T10:30:01.000Z","token":"a3b7x9k2m5p8q1w4","domain":"example.com","config":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"},"preferences":{"enabled":[],"disabled":[]},"userAgent":"MyApp/2.1.0 (iOS 17.0; iPhone)","pathname":"/","referrer":null},{"type":"consent_accepted","timestamp":"2025-09-18T10:30:05.000Z","token":"a3b7x9k2m5p8q1w4","domain":"example.com","config":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"},"preferences":{"enabled":["essential","analytics","marketing"],"disabled":[]},"userAgent":"MyApp/2.1.0 (iOS 17.0; iPhone)","pathname":"/","referrer":null}]},"partial-consent-with-settings":{"summary":"User opens settings, toggles categories, partial consent","value":[{"type":"page_view","timestamp":"2025-09-18T10:30:00.000Z","token":"a3b7x9k2m5p8q1w4","domain":"example.com","config":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"},"preferences":{"enabled":[],"disabled":[]},"userAgent":"MyApp/2.1.0 (iOS 17.0; iPhone)","pathname":"/","referrer":null},{"type":"consent_displayed","timestamp":"2025-09-18T10:30:01.000Z","token":"a3b7x9k2m5p8q1w4","domain":"example.com","config":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"},"preferences":{"enabled":[],"disabled":[]},"userAgent":"MyApp/2.1.0 (iOS 17.0; iPhone)","pathname":"/","referrer":null},{"type":"settings_opened","timestamp":"2025-09-18T10:30:03.000Z","token":"a3b7x9k2m5p8q1w4","domain":"example.com","config":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"},"preferences":{"enabled":[],"disabled":[]},"userAgent":"MyApp/2.1.0 (iOS 17.0; iPhone)","pathname":"/","referrer":null},{"type":"category_toggled","timestamp":"2025-09-18T10:30:05.000Z","token":"a3b7x9k2m5p8q1w4","domain":"example.com","config":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"},"preferences":{"enabled":["essential","analytics"],"disabled":["marketing"]},"userAgent":"MyApp/2.1.0 (iOS 17.0; iPhone)","pathname":"/","referrer":null},{"type":"consent_partial","timestamp":"2025-09-18T10:30:07.000Z","token":"a3b7x9k2m5p8q1w4","domain":"example.com","config":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"},"preferences":{"enabled":["essential","analytics"],"disabled":["marketing"]},"userAgent":"MyApp/2.1.0 (iOS 17.0; iPhone)","pathname":"/","referrer":null}]}}}}},"responses":{"200":{"description":"Events successfully proxied to the events backend. The response is passed through from the backend.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventSubmissionResponse"}}}},"400":{"description":"Validation error (headless mode only). When `X-Mobile-Platform: headless` is set, the gateway validates events against the HeadlessEvent schema. If any event fails, the entire batch is rejected. For non-headless clients, 400 responses come from the backend and may have a different shape.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeadlessEventValidationError"},"examples":{"invalid-event-type":{"summary":"Invalid event type","value":{"error":"validation_error","message":"must be equal to one of the allowed values","timestamp":"2025-09-18T10:30:00.000Z","details":[{"eventIndex":0,"errors":[{"field":"type","message":"must be equal to one of the allowed values","constraint":"enum","allowedValues":["page_view","consent_displayed","consent_accepted","consent_rejected","consent_partial","settings_opened","category_toggled","app_att_authorized","app_att_denied"],"receivedValue":"custom_event"}]}],"allowedEventTypes":["page_view","consent_displayed","consent_accepted","consent_rejected","consent_partial","settings_opened","category_toggled","app_att_authorized","app_att_denied"]}},"missing-required-fields":{"summary":"Missing required fields","value":{"error":"validation_error","message":"must have required property 'token'","timestamp":"2025-09-18T10:30:01.000Z","details":[{"eventIndex":0,"errors":[{"field":"token","message":"must have required property 'token'","constraint":"required"}]}],"allowedEventTypes":["page_view","consent_displayed","consent_accepted","consent_rejected","consent_partial","settings_opened","category_toggled","app_att_authorized","app_att_denied"]}},"missing-backend-required-field":{"summary":"Missing config/preferences/userAgent/pathname/referrer (backend-required, not just the legacy 4-field set)","value":{"error":"validation_error","message":"must have required property 'config'","timestamp":"2025-09-18T10:30:02.000Z","details":[{"eventIndex":0,"errors":[{"field":"config","message":"must have required property 'config'","constraint":"required"}]}],"allowedEventTypes":["page_view","consent_displayed","consent_accepted","consent_rejected","consent_partial","settings_opened","category_toggled","app_att_authorized","app_att_denied"]}},"invalid-format":{"summary":"Payload is not a JSON array","value":{"error":"invalid_format","message":"Request body must be a non-empty JSON array of event objects (max 100 events)","timestamp":"2025-09-18T10:30:00.000Z"}}}}}},"401":{"description":"Unauthorized — invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"unauthorized":{"summary":"Missing or invalid token","value":{"error":"unauthorized","message":"Invalid or missing bearer token"}}}}}},"405":{"description":"Method not allowed — only POST is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"method-not-allowed":{"summary":"Non-POST request","value":{"error":"method_not_allowed","message":"Only POST method is supported for analytics events"}}}}}},"413":{"description":"Payload too large — request body exceeds 100 KB","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"payload-too-large":{"summary":"Body exceeds 100 KB limit","value":{"error":"payload_too_large","message":"Request body must not exceed 102400 bytes","timestamp":"2025-09-18T10:30:00.000Z"}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"rate-limit":{"summary":"Rate limit exceeded","value":{"error":"rate_limit_exceeded","message":"Request rate limit exceeded. Please wait before sending more events."}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/terms/{projectId}/{configId}":{"get":{"tags":["Mobile SDK"],"summary":"Get Terms & Conditions content","description":"Retrieve normalized Terms & Conditions content for a project configuration. The backend collection is `contractsV2`; `terms` is the public alias used in this API. Returns structured content including display strings, sections, and a link to the PDF version.","operationId":"mobile_getTermsContent","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-fA-F]{24}$"},"description":"Project identifier (MongoDB ObjectId)","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"configId","in":"path","required":true,"schema":{"type":"string"},"description":"Configuration identifier. May be a 24-hex MongoDB ObjectId or a slug. For contractsV2 configs, a `~consentFor` suffix is supported (e.g. `terms_of_sale~consentFor:cookies_config_id`).","example":"terms_of_sale"},{"name":"tag","in":"query","required":false,"schema":{"type":"string","default":"latest"},"description":"Version tag to retrieve. Defaults to `latest`.","example":"latest"}],"responses":{"200":{"description":"Successfully retrieved Terms & Conditions content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TermsContent"},"examples":{"terms-content-response":{"summary":"Terms & Conditions content","value":{"projectId":"67fcdb2b52ab9a99a5865f4d","identifier":"terms_of_sale","language":"fr","tag":"latest","versionName":"v1.2","publishedAt":"2026-04-01T00:00:00.000Z","pdfUrl":"https://storage.axeptio.eu/terms/67fcdb2b52ab9a99a5865f4d/terms_of_sale/v1.2.pdf","content":{"strings":{"title":"Conditions Générales de Vente","acceptButton":"J'accepte","declineButton":"Je refuse"},"sections":[{"id":"section-1","title":"Objet","body":"Les présentes conditions régissent l'utilisation du service."}]}}}}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"403":{"description":"Access denied to this project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Project, configuration, or Terms & Conditions content not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/terms/{projectId}/{configId}/pdf":{"get":{"tags":["Mobile SDK"],"summary":"Download Terms & Conditions PDF","description":"Stream the PDF version of the Terms & Conditions document for a project configuration. Returns `application/pdf` content directly. The backend collection is `contractsV2`; `terms` is the public alias used in this API.","operationId":"mobile_getTermsPdf","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-fA-F]{24}$"},"description":"Project identifier (MongoDB ObjectId)","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"configId","in":"path","required":true,"schema":{"type":"string"},"description":"Configuration identifier. May be a 24-hex MongoDB ObjectId or a slug. For contractsV2 configs, a `~consentFor` suffix is supported.","example":"terms_of_sale"},{"name":"tag","in":"query","required":false,"schema":{"type":"string","default":"latest"},"description":"Version tag to retrieve. Defaults to `latest`.","example":"latest"}],"responses":{"200":{"description":"PDF document stream","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"401":{"description":"Authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"403":{"description":"Access denied to this project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"No PDF available for this Terms & Conditions configuration","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"Bad gateway — upstream PDF storage unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/public/geolocation/{projectId}":{"get":{"tags":["Public"],"summary":"Resolve TCF vs Brands from the visitor's location (no auth)","description":"Given the visitor's Cloudflare-derived location, returns which flow the project serves them — `tcf` or `brands` — and the config id within it. Built for the Axeptio Google Tag Manager template, which must decide before `static.axept.io/sdk.js` loads.\n\n**Unauthenticated by design.** The inputs are already public: the widget itself fetches `client.axept.io/{projectId}.json` and `client.axept.io/tcf/{projectId}.json` from every visitor's browser without credentials, and those documents carry the full config list including all country/subdivision targeting. The visitor's geolocation is their own IP. This endpoint computes a view over public data and discloses nothing a caller could not derive with two public fetches. Its authenticated sibling `/geolocation/{projectId}` is unchanged and remains the endpoint for headless SDK integrations.\n\n**Two representations.** Append `.js` to the path to get an executable snippet that assigns `window.axeptioSettings.cookiesVersion` and `.flowType` instead of JSON. That form exists because a Google Tag Manager *web* template cannot read an HTTP response at all — its sandbox has no fetch/XHR, `sendHttpGet` is server-side-GTM only, and `sendPixel` is fire-and-forget — so the only response a container can consume is a script it injects. The `.js` variant returns JavaScript for error statuses too, so `injectScript`'s failure callback fires cleanly rather than the page hitting a syntax error.\n\n**Flow selection** is by match specificity, not a fixed preference: `country-exact` beats `subdivision` beats `regulation` beats `fallback`, compared across both flows, so a TCF config targeting the visitor's country wins over a Brands catch-all and vice versa. Ties resolve to TCF. Responses are cached for 300s keyed on the full geo tuple and returned `private`, since the body varies by visitor country.","operationId":"resolvePublicGeolocation","security":[],"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{24}$"},"description":"Project identifier (24 lowercase hex). Append `.js` to the path for the injectable script form.","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"language","in":"query","required":false,"schema":{"type":"string"},"description":"Preferred language; breaks ties between configs that match the visitor's location equally well.","example":"fr"},{"name":"platform","in":"query","required":false,"schema":{"type":"string","enum":["web","mobile"],"default":"web"},"description":"Narrows to configs tagged for this platform. Defaults to `web` — every caller of this endpoint is a browser. An unrecognised value collapses to the default."},{"name":"country","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Za-z]{2}$"},"description":"Override the detected country. **Ignored in production** — testing aid only, so a visitor can never talk themselves into another jurisdiction's banner.","example":"FR"},{"name":"region","in":"query","required":false,"schema":{"type":"string"},"description":"Override the detected ISO subdivision code, e.g. `CA` for California or `QC` for Quebec. Ignored in production, and only read when `country` is also supplied.","example":"QC"}],"responses":{"200":{"description":"Resolved flow and config. Returns `application/javascript` when the path ends in `.js`.","headers":{"X-Cache":{"schema":{"type":"string","enum":["HIT","MISS"]},"description":"Whether the resolution was served from the worker's KV cache."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicGeolocationResponse"}},"application/javascript":{"schema":{"type":"string"},"example":"window.axeptioSettings = window.axeptioSettings || {};\nwindow.axeptioSettings.cookiesVersion = \"6904ca84683c996cd3788342\";\nwindow.axeptioSettings.flowType = \"tcf\";\n"}}},"404":{"description":"No configuration matches the visitor's location, or the project is unknown"},"405":{"description":"Method not allowed — GET only"},"500":{"description":"Unexpected error"},"502":{"description":"Upstream configuration service returned an error"},"503":{"description":"Upstream configuration service unreachable"}}}},"/mobile/geolocation/{projectId}":{"get":{"tags":["Mobile SDK"],"summary":"Resolve visitor geolocation, regulation, and applicable config","description":"Crosses the visitor's Cloudflare-derived geolocation with the project's cookies configurations to return the applicable regulation (GDPR, CCPA, UK GDPR, FADP, Loi 25) and the most relevant configId, mirroring the web widget's selection behavior. The caller then fetches vendors via /mobile/vendors/{projectId}/{configId}. Incompatible with defaultConfigIdEnforcement — returns 409 when enforcement is active.","operationId":"mobile_resolveGeolocation","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"},"description":"Project identifier","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"language","in":"query","required":false,"schema":{"type":"string"},"description":"ISO 639-1 language preference for config tie-breaking","example":"fr"},{"name":"platform","in":"query","required":false,"schema":{"type":"string","enum":["mobile","web"],"default":"mobile"},"description":"Preferred platform flavor of the resolved config. Falls back to any available config if no platform-specific variant exists."},{"name":"country","in":"query","required":false,"schema":{"type":"string"},"description":"(Non-production only) Override the detected country — e.g. FR, US, GB. Ignored in production."},{"name":"region","in":"query","required":false,"schema":{"type":"string"},"description":"(Non-production only) Override the detected subdivision — e.g. QC. Ignored in production."}],"responses":{"200":{"description":"Geolocation resolved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeolocationResponse"},"examples":{"fr-mobile":{"summary":"French visitor, mobile platform","value":{"geolocation":{"countryCode":"FR","subdivisionCode":"IDF","source":"cloudflare"},"regulation":{"code":"gdpr","internalIsoCode":"EU","isDefault":false},"resolvedConfig":{"configId":"6859079473219bcbb8435079","matchMode":"regulation","language":"fr","platform":"mobile","isFallback":false,"platformNarrowed":true},"next":{"vendorsEndpoint":"/mobile/vendors/67fcdb2b52ab9a99a5865f4d/6859079473219bcbb8435079"},"availableRegulations":["gdpr","uk_gdpr","fadp","ccpa","loi25"]}},"unknown-country-gdpr-default":{"summary":"Unmapped country — defaults to GDPR","value":{"geolocation":{"countryCode":"Unknown","subdivisionCode":null,"source":"none"},"regulation":{"code":"gdpr","internalIsoCode":"EU","isDefault":true},"resolvedConfig":{"configId":"6859079473219bcbb8435079","matchMode":"regulation","language":"en","platform":"mobile","isFallback":false,"platformNarrowed":true},"next":{"vendorsEndpoint":"/mobile/vendors/67fcdb2b52ab9a99a5865f4d/6859079473219bcbb8435079"},"availableRegulations":["gdpr","uk_gdpr","fadp","ccpa","loi25"]}}}}}},"401":{"description":"Unauthorized — invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"403":{"description":"Forbidden — token does not own this project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"No configuration matches the visitor's geolocation (or the project does not exist)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only GET is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"409":{"description":"Enforcement conflict — defaultConfigIdEnforcement is active on the project. Set it to 'off' (or 'warn') in the BackOffice to use this endpoint.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"enforcement-active":{"summary":"Enforcement conflict","value":{"error":"enforcement_conflict","message":"Geolocation resolution is disabled while defaultConfigIdEnforcement is active. Set it to 'off' (or 'warn') in the BackOffice to use this endpoint."}}}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"Upstream configuration service returned a bad gateway error. Propagated from the upstream status carried by ConfigFetchError.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"Upstream configuration service unavailable or network failure reaching it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"504":{"description":"Upstream configuration service timed out. Propagated from the upstream status carried by ConfigFetchError.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/stats":{"get":{"tags":["Analytics"],"summary":"Read-only project consent statistics (Looker Studio)","description":"Returns aggregated consent statistics for the project resolved from the bearer token. Designed for BI tools (Looker Studio, Power BI, Tableau). The response has three blocks: `meta` (applied filters), `summary` (totals over the whole period), and `data` (one row per `groupBy` combination). Both `summary` and each `data` row also carry a `responses` array — the `consent`/`partial`/`reject` counts pivoted into rows so a BI tool has a dimension to chart a pie against; it is derived from those fields and adds no new data. This endpoint is NOT under the /mobile prefix.","operationId":"getLookerStats","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"Start date (inclusive), YYYY-MM-DD. Must be provided together with 'to'; omit both for the default last-30-days window. A single bound is rejected with 400.","example":"2026-01-01"},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date"},"description":"End date (inclusive), YYYY-MM-DD. Must be provided together with 'from'; omit both for the default last-30-days window. A single bound is rejected with 400.","example":"2026-01-31"},{"name":"groupBy","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated dimensions. Allowed values: date, country, device, configVersion. Defaults to date.","example":"date,country"},{"name":"projectId","in":"query","required":false,"schema":{"type":"string","pattern":"^[a-fA-F0-9]{24}$"},"description":"Project to report on. **Required when authenticating with a Frontegg token** (`fronteggAuth`), which carries no project claim — omitting it is a 400, and access to the named project is authorized per request. **Ignored-if-matching, rejected-if-not when authenticating with a headless bearer token** (`bearerAuth`), whose project is the token itself: naming a different project returns 403 rather than silently answering for the token's own project.","example":"5df0bad6730c99249ab3c348"}],"responses":{"200":{"description":"Aggregated statistics for the project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsResponse"}}}},"400":{"description":"Invalid parameter (bad date format, unknown groupBy value, range over 365 days, or a missing/malformed projectId on a Frontegg-authenticated call)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"401":{"description":"Unauthorized - Invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"403":{"description":"The authenticated identity may not read the requested project's statistics. Also returned when a project does not exist, deliberately: the endpoint does not distinguish the two, so it cannot be used to probe which project ids are real.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"The stats backend returned an error — a query or runtime failure. Authentication failures against the stats backend are reported as 503, not 502.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"Stats backend not configured or temporarily unavailable. Also returned when the stats backend rejects our credentials, which is a configuration failure on our side and not something the caller can act on beyond retrying later; the two cases are deliberately indistinguishable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"504":{"description":"The stats backend timed out","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]},{"fronteggAuth":[]}]}},"/mobile/tcf/decode":{"get":{"tags":["TCF"],"summary":"Decode an IAB TCF v2.x consent string","description":"Stateless decoder for IAB TCF v2.x TC strings. Validates only the TCF *major* version (== 2); declared version 1 or 3-7 surface as `unsupported_version`, anything else as `bad_format`. The decode operation itself never touches KV — but the endpoint is still gated by the standard bearer-token auth and per-project rate limiter (shared across the project's tokens), both of which read KV. Returns a JSON-serializable view of the underlying TCModel: cmpId, cmpVersion, vendorListVersion, tcfPolicyVersion, publisherCC, timestamps, and arrays of integer ids for purposes / vendors / specialFeatures / publisher consents. Successful responses include `Cache-Control: public, max-age=300`. `X-RateLimit-*` headers are present on every response that is returned **after** the auth + rate-limit checks (200, 400 validation/decoder errors, 429, 5xx); the early-return 401 / 404-routing / 405 responses do not carry them. 429 responses additionally include `Retry-After` (seconds).","operationId":"mobile_decodeTCString","parameters":[{"name":"tc","in":"query","required":true,"schema":{"type":"string"},"description":"Base64url-encoded TCF v2.x consent string (may include an optional `.publisherTC` segment). Required; missing/empty values are reported as `invalid_argument`, malformed values as `bad_format`, and TCF v1/v3+ as `unsupported_version`.","example":"COtybn4OtybnEAAABAENAA"}],"responses":{"200":{"description":"Successfully decoded TC string","headers":{"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TCDecodedResponse"}}}},"400":{"description":"Bad request — `invalid_argument` (missing `tc`), `bad_format` (malformed TC string), or `unsupported_version` (TCF v1 or v3+). Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"missing-tc":{"summary":"Missing tc query parameter","value":{"error":"invalid_argument","message":"Missing required query parameter: tc"}},"bad-format":{"summary":"Malformed TC string","value":{"error":"bad_format","message":"Invalid TCF consent string"}},"unsupported-version":{"summary":"Unsupported TCF major version (e.g. v1)","value":{"error":"unsupported_version","message":"TCF v1 is not supported"}}}}}},"401":{"description":"Unauthorized — invalid or missing bearer token. Returned before the rate-limit check, so rate-limit headers are not present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Not found — paths under `/mobile/tcf/decode/...` are not served. Returned before auth, so rate-limit headers are not present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only GET is supported. Returned before auth, so rate-limit headers are not present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded — limits are project-scoped (shared across all of a project's tokens), not per-token.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"Service unavailable — `gvl_missing`. Reserved for future GVL-dependent decode paths; the current handler does not raise this on the bitfield-only decode path, but the status-mapping table in `decode-endpoint.js` documents it as part of the error contract.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/encode":{"post":{"tags":["TCF"],"summary":"Encode an IAB TCF v2.x consent string","description":"Server-side encoder: turns TCModel-shape JSON (a user's purpose / vendor / special-feature choices) into a spec-compliant IAB TCF v2.x TC string, so app developers never have to bundle or correctly drive an IAB encoder client-side. The inverse of GET /mobile/tcf/decode — the returned `tcString` round-trips back through decode to the same signals. Unlike decode, encode is NOT stateless: it loads the GVL for the requested `vendorListVersion` from the worker's daily-refreshed KV cache to drive the encoding (no on-demand fetch — the version must already be cached). Gated by the standard bearer-token auth and per-project rate limiter (limits are shared across all of a project's tokens). The JSON body is capped at 100 KB (`payload_too_large`, 413). `X-RateLimit-*` headers are present on every response returned **after** the auth + rate-limit checks (200, 400, 413, 429, 5xx); the early-return 401 / 404-routing / 405 responses do not carry them. 429 responses additionally include `Retry-After` (seconds).","operationId":"mobile_encodeTCString","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TcfEncodeRequest"}}}},"responses":{"200":{"description":"Successfully encoded TC string","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TcfEncodeResponse"}}}},"400":{"description":"Bad request — `invalid_argument` (body is not an object, or `cmpId` / `cmpVersion` / `vendorListVersion` missing or ill-typed), `bad_format` (body is not valid JSON, or the model is un-encodable, e.g. a reserved `cmpId` of 0/1 or an invalid `publisherCC`), or `unknown_gvl_version` (the requested `vendorListVersion` is not cached). Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"missing-vendor-list-version":{"summary":"Missing or non-positive vendorListVersion","value":{"error":"invalid_argument","message":"vendorListVersion is required and must be a positive integer"}},"bad-format":{"summary":"Un-encodable model (e.g. reserved cmpId)","value":{"error":"bad_format","message":"Unable to encode the provided TCF model"}},"unknown-gvl-version":{"summary":"Requested GVL version is not cached","value":{"error":"unknown_gvl_version","message":"No cached GVL for the requested vendorListVersion"}}}}}},"401":{"description":"Unauthorized — invalid or missing bearer token. Returned before the rate-limit check, so rate-limit headers are not present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Not found — paths under `/mobile/tcf/encode/...` are not served. Returned before auth, so rate-limit headers are not present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only POST is supported. Returned before auth, so rate-limit headers are not present.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"413":{"description":"Payload too large — the JSON body exceeds the 100 KB cap (`payload_too_large`). Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded — limits are project-scoped (shared across all of a project's tokens), not per-token.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"Service unavailable — `gvl_missing`: the TCF_GVL store binding is absent or temporarily unavailable, so the GVL needed to encode could not be loaded. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/configurations/{projectId}/{configId}":{"get":{"tags":["TCF"],"summary":"Get mobile-optimized TCF configuration (GVL projection)","description":"Returns a mobile-optimized projection of the IAB TCF Global Vendor List for a specific project configuration: localized purposes / special purposes / features / special features / stacks, the vendor list with per-vendor purpose/feature id arrays, and publisher restrictions. Served from KV with `Cache-Control: public, max-age=300`; `X-Cache: HIT|MISS` indicates whether the projection was cache-served. Bearer-gated and tenant-scoped: the token's projectId must match the path projectId (403 otherwise).","operationId":"mobile_getTcfConfiguration","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{24}$"},"description":"Project identifier (24-char hex MongoDB ObjectId).","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"configId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{24}$"},"description":"TCF configuration identifier (24-char hex MongoDB ObjectId).","example":"6859079473219bcbb8435079"},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en"},"description":"BCP47 language tag for localized GVL labels. Missing/empty defaults to \"en\"; an invalid tag yields invalid_argument (400).","example":"fr"}],"responses":{"200":{"description":"TCF configuration projection","headers":{"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"X-Cache":{"schema":{"type":"string","enum":["HIT","MISS"]},"description":"Whether the projection was served from KV cache."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TcfConfigurationResponse"}}}},"400":{"description":"Bad request — invalid_argument (bad lang tag, or malformed projectId/configId).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"401":{"description":"Unauthorized — invalid or missing bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"403":{"description":"Forbidden — the bearer token's projectId does not match the path projectId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Not found — the widget/cookie configuration for this project/config does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only GET is supported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error — bad_response (corrupt KV payload).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"Bad gateway — upstream config service unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"Service unavailable — kv_unavailable (TCF_VENDORS / TCF_GVL binding missing) or gvl_missing (GVL pipeline has not seeded KV yet).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/standard-info/{projectId}/{configId}":{"get":{"tags":["TCF"],"summary":"Get the TCF standard-information notice for native UIs","description":"Returns the publisher's TCF \"standard information\" notice for a configuration, ready for a native app card UI (MSK-213 Option A). The publisher `introduction` text is returned both verbatim (`introduction.raw`) and with every dynamic placeholder resolved server-side (`introduction.html`) — `[VENDOR_COUNT]`, `[REFUSE_OR_WITHDRAW_BUTTON_TEXT]`, `[I_CHOOSE_BUTTON_TEXT]`, `[ON_THIS_WEBSITE_OR_APP]`, `[OUR_WEBSITE_OR_APP]`, `[MANAGE_COOKIES_LINK_OR_BUTTON_TEXT]`. Those same pre-calculated values are also exposed discretely under `variables`, and the GVL purposes/stacks are returned as ordered `cards` (each with a per-section vendor count). Served with `Cache-Control: public, max-age=300`; `X-Cache: HIT|MISS`. Bearer-gated and tenant-scoped: the token's projectId must match the path projectId (403 otherwise).","operationId":"mobile_getTcfStandardInformation","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{24}$"},"description":"Project identifier (24-char hex MongoDB ObjectId).","example":"67fcdb2b52ab9a99a5865f4d"},{"name":"configId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-f0-9]{24}$"},"description":"TCF configuration identifier (24-char hex MongoDB ObjectId).","example":"6859079473219bcbb8435079"},{"name":"lang","in":"query","required":false,"schema":{"type":"string","default":"en"},"description":"Language tag: a two-letter language code, optionally with a two-letter region (`lang` or `lang-region`, e.g. `fr` or `fr-CA`); other forms (script, subdivision, three-letter codes) yield invalid_argument (400). Localizes text + GVL labels directly from this tag (English fallback when a translation is missing). Also selects the language of the `introduction` notice — see that field for its per-banner sourcing. Missing/empty defaults to \"en\".","example":"fr"},{"name":"context","in":"query","required":false,"schema":{"type":"string","enum":["app","web"],"default":"app"},"description":"Which phrasing to use for the contextual placeholders and button labels. Defaults to \"app\" (native SDK); \"web\" yields website-oriented wording. Any other value yields invalid_argument (400).","example":"app"}],"responses":{"200":{"description":"Standard-information payload","headers":{"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"X-Cache":{"schema":{"type":"string","enum":["HIT","MISS"]},"description":"Whether the payload was served from KV cache."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TcfStandardInformationResponse"}}}},"400":{"description":"Bad request — invalid_argument (bad lang tag, bad context, or malformed projectId/configId).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"401":{"description":"Unauthorized — invalid or missing bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"403":{"description":"Forbidden — the bearer token's projectId does not match the path projectId.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Not found — the configuration for this project/config does not exist.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only GET is supported.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Too many requests — rate_limit_exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error — bad_response (corrupt KV payload) or an unexpected error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"Bad gateway — upstream config or TCF text service unreachable.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"Service unavailable — kv_unavailable (TCF_VENDORS / TCF_GVL binding missing) or gvl_missing (GVL pipeline has not seeded KV yet).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/gvl/latest":{"get":{"tags":["TCF"],"summary":"Get the latest Global Vendor List (GVL) snapshot","description":"Returns the most recent GVL snapshot cached in KV. The snapshot is refreshed daily by a scheduled task — this endpoint never triggers a network fetch. If KV has not been seeded yet, returns 404 `no_latest`. Successful responses include `Cache-Control: public, max-age=300` and `X-RateLimit-*` headers.","operationId":"mobile_getTcfGvlLatest","responses":{"200":{"description":"Latest GVL JSON","headers":{"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GVLResponse"}}}},"401":{"description":"Unauthorized — invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"No GVL snapshot seeded yet (`no_latest`) — wait for the next scheduled refresh or request a manual seed. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"no-latest":{"summary":"GVL not seeded","value":{"error":"no_latest","message":"No gvl:latest pointer in KV — call refreshLatest first"}}}}}},"405":{"description":"Method not allowed — only GET is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded — limits are project-scoped (shared across all of a project's tokens), not per-token.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error — including KV inconsistency where `gvl:latest` points at a missing snapshot. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"GVL store unavailable (`kv_unavailable`) — TCF_GVL binding missing on the worker. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/gvl/{version}":{"get":{"tags":["TCF"],"summary":"Get a specific archived GVL version","description":"Returns the GVL snapshot for the requested version. `{version}` is the IAB **`vendorListVersion`** — a large, monotonically increasing integer (e.g. 348), **not** the TCF framework major version (which is 2.x). For the current list, use `GET /mobile/tcf/gvl/latest`.\n\nIf the version is already cached in KV it is returned directly; otherwise it is fetched on-demand from the IAB archive mirror, cached, and returned. A version that does not exist on the mirror returns 404 `not_found`. If the mirror is unreachable, 502 `fetch_failed` is returned.","operationId":"mobile_getTcfGvlVersion","parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"integer","minimum":1},"description":"IAB GVL vendorListVersion (a large positive integer, e.g. 348 — not the TCF 2.x framework version). Use /mobile/tcf/gvl/latest for the current list.","example":348}],"responses":{"200":{"description":"Archived GVL JSON for the requested version","headers":{"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GVLResponse"}}}},"400":{"description":"`invalid_argument` — the `{version}` path value is not a positive integer (e.g. `0`). Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"non-positive-version":{"summary":"Version is zero or negative","value":{"error":"invalid_argument","message":"Invalid GVL version (expected positive integer)"}}}}}},"401":{"description":"Unauthorized — invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"The requested version does not exist on the IAB archive mirror (`not_found`) — it was neither cached nor fetchable. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only GET is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded — limits are project-scoped (shared across all of a project's tokens), not per-token.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"`fetch_failed` — the IAB archive mirror was unreachable or errored while fetching this version on-demand. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"GVL store unavailable (`kv_unavailable`) — TCF_GVL binding missing on the worker. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/gvl/status":{"get":{"tags":["TCF"],"summary":"GVL cache status / health","description":"Diagnostic snapshot of the GVL cache: which version `latest` points at and which archived versions are currently seeded in KV. `ok` is `false` (with `latest: null`) when the GVL pipeline has not seeded KV yet — useful when a version request unexpectedly 404s.","operationId":"mobile_getTcfGvlStatus","responses":{"200":{"description":"GVL cache status","headers":{"Cache-Control":{"description":"Diagnostic responses are never cached.","schema":{"type":"string","example":"no-store"}},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GVLStatusResponse"}}}},"401":{"description":"Unauthorized — invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only GET is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded — limits are project-scoped (shared across all of a project's tokens), not per-token.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"`bad_response` — the `gvl:latest` pointer in KV is corrupt (unexpected shape or invalid JSON). Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"GVL store unavailable (`kv_unavailable`) — TCF_GVL binding missing on the worker. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/gvl/{version}/lang/{lang}":{"get":{"tags":["TCF"],"summary":"Get a translated GVL purposes payload for a specific version + language","description":"Returns translated purposes for the given GVL version and BCP47 language tag (e.g. `fr`, `pt-br`). Translations are normalised to lowercase by gvl-store. A missing translation surfaces as 404 `not_found` — this endpoint never triggers a network fetch.","operationId":"mobile_getTcfGvlVersionLang","parameters":[{"name":"version","in":"path","required":true,"schema":{"type":"integer","minimum":1},"description":"GVL version number (positive integer)","example":348},{"name":"lang","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z]{2}(-[A-Za-z]{2})?$"},"description":"BCP47 language tag — `lang` or `lang-region` (case-insensitive; normalised to lowercase)","example":"fr"}],"responses":{"200":{"description":"Translated purposes for the requested version + language","headers":{"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GVLLangResponse"}}}},"400":{"description":"`invalid_argument` — the `{version}` path value is not a positive integer (e.g. `0`). Returned after auth + rate-limit checks, so rate-limit headers are present. `{lang}` is constrained by the route regex (`^[A-Za-z]{2}(-[A-Za-z]{2})?$`) so a malformed language tag surfaces as 404, not 400.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"non-positive-version":{"summary":"Version is zero or negative","value":{"error":"invalid_argument","message":"Invalid GVL version (expected positive integer)"}}}}}},"401":{"description":"Unauthorized — invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Translation not cached for this version + language (`not_found`). Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only GET is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded — limits are project-scoped (shared across all of a project's tokens), not per-token.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"GVL store unavailable (`kv_unavailable`) — TCF_GVL binding missing on the worker. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/gvl/latest/lang/{lang}":{"get":{"tags":["TCF"],"summary":"Get the latest GVL translated purposes for a language","description":"Returns translated purposes for the latest GVL version and the given BCP47 language tag (e.g. `fr`, `pt-br`), resolving the current version server-side via the `gvl:latest` pointer — callers don't need to know or fetch the version number first. If KV has not been seeded yet, returns 404 `no_latest`. If the translation isn't already cached it is fetched on demand from the IAB mirror (which serves the current GVL's translation) and cached; a language the mirror doesn't serve returns 404 `not_found`, and an upstream outage returns 502 `fetch_failed`. English (`en`) is the GVL's base language — there is no English translation file on the mirror, so it is served from the base GVL snapshot itself (no network fetch), and `en` therefore resolves like any other available language. Successful responses include `Cache-Control: public, max-age=300` and `X-RateLimit-*` headers.","operationId":"mobile_getTcfGvlLatestLang","parameters":[{"name":"lang","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z]{2}(-[A-Za-z]{2})?$"},"description":"BCP47 language tag — `lang` or `lang-region` (case-insensitive; normalised to lowercase)","example":"fr"}],"responses":{"200":{"description":"Translated purposes for the latest version + language","headers":{"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GVLLangResponse"}}}},"401":{"description":"Unauthorized — invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"Either no GVL snapshot seeded yet (`no_latest`) or the IAB mirror does not serve a translation for the requested language (`not_found`). Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"no-latest":{"summary":"GVL not seeded","value":{"error":"no_latest","message":"No gvl:latest pointer in KV — call refreshLatest first"}},"not-found":{"summary":"Language not served by the IAB mirror","value":{"error":"not_found","message":"GVL translation not available for this language"}}}}}},"405":{"description":"Method not allowed — only GET is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded — limits are project-scoped (shared across all of a project's tokens), not per-token.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"Internal server error. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"Bad gateway (`fetch_failed`) — the IAB mirror was unreachable or errored while lazily fetching the translation. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"GVL store unavailable (`kv_unavailable`) — TCF_GVL binding missing on the worker. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/tcf/texts":{"get":{"tags":["TCF"],"summary":"Get combined UI label + GVL reference texts (keyed, language-aware)","description":"Returns every localized string a developer needs to build their own TCF UI, in one call: `texts.ui` holds the ~66 keyed UI label strings (buttons, titles, section labels — placeholders like `{0}` are preserved for client-side interpolation), and `texts.{purposes,specialPurposes,features,specialFeatures,stacks,dataCategories}` hold the IAB GVL reference texts projected to `{ id, name, description }` only (vendors and structural arrays are stripped — use `/mobile/tcf/gvl/*` for the full GVL).\n\nThe UI labels come from the bundled English pack overlaid with the locale pack from `client.axept.io/pack-tcf/{locale}`; a locale with no pack degrades to English and sets `uiFallback: \"en\"`. GVL texts come from the cached GVL (`gvl:latest`), using the `gvl:{version}:lang:{lang}` translation when present and falling back per-category to English otherwise. Responses are cached per `gvlVersion`+locale (`X-Cache: HIT|MISS`).","operationId":"mobile_getTcfTexts","parameters":[{"name":"lang","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Za-z]{2}(-[A-Za-z]{2})?(-[A-Za-z0-9]{1,3})?$","default":"en"},"description":"BCP47 locale — `lang`, `lang-region`, or `lang-region-subdivision` (e.g. `fr`, `fr-CA`, `fr-CA-GC`). Defaults to `en`. The GVL translation lookup uses `lang` or `lang-region` (the subdivision subtag is dropped).","example":"fr-CA"}],"responses":{"200":{"description":"Combined UI + GVL texts for the requested locale","headers":{"Cache-Control":{"$ref":"#/components/headers/CacheControl"},"X-Cache":{"schema":{"type":"string","enum":["HIT","MISS"]},"description":"Whether the assembled payload was served from the API_CACHE KV"},"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TcfTextsResponse"}}}},"400":{"description":"`invalid_argument` — the `lang` query parameter is not a valid BCP47 tag. Returned before auth.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"},"examples":{"bad-lang":{"summary":"Malformed language tag","value":{"error":"invalid_argument","message":"Invalid lang query parameter — expected BCP47 lang, lang-region, or lang-region-subdivision"}}}}}},"401":{"description":"Unauthorized — invalid or missing bearer token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"404":{"description":"`not_found` — a stray sub-path under the routed prefix (e.g. `/mobile/tcf/texts/foo`). Only the base `/mobile/tcf/texts` is valid; deeper paths return a controlled 404. Returned before auth.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"405":{"description":"Method not allowed — only GET is supported","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"429":{"description":"Rate limit exceeded — limits are project-scoped (shared across all of a project's tokens), not per-token.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"500":{"description":"`internal_error` — `gvl:latest` points at a missing snapshot; or `bad_response` — a corrupt GVL KV entry. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"502":{"description":"`bad_gateway` — the UI text service (`client.axept.io/pack-tcf`) was unreachable or returned a server error. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}},"503":{"description":"`gvl_missing` — the GVL pipeline has not seeded KV yet; or `kv_unavailable` — the TCF_GVL binding is missing on the worker. Returned after auth + rate-limit checks, so rate-limit headers are present.","headers":{"X-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse1"}}}}},"security":[{"bearerAuth":[]}]}},"/mobile/client/{projectId}/consents/{token}":{"get":{"tags":["Consent Reading"],"summary":"Retrieve single consent record by token","description":"Retrieve a specific consent record using a consent token. This is the primary endpoint used by mobile applications and widgets to fetch individual consent records. Accepts optional query parameters for service and identifier.\n\nFor records whose stored `preferences.tcString` decodes successfully, the response body is decorated with an optional top-level `decoded` field carrying the parsed `TCDecodedResponse` (see `ConsentReadResponse`). Records without a `preferences.tcString`, or whose stored `preferences.tcString` fails to decode, are returned exactly as stored.","operationId":"getSingleConsent","security":[{"bearerAuth":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"description":"Project identifier (MongoDB ObjectId)","schema":{"type":"string","pattern":"^[a-f\\d]{24}$"}},{"name":"token","in":"path","required":true,"description":"Consent token","schema":{"type":"string","minLength":1,"maxLength":255}},{"name":"service","in":"query","required":false,"description":"Service type (defaults to 'cookies'). 'terms' is the public alias for 'contractsV2'.","schema":{"type":"string","enum":["cookies","processings","contracts","contractsV2","terms"],"default":"cookies"}},{"name":"identifier","in":"query","required":false,"description":"Configuration identifier (MongoDB ObjectId, defaults to generic identifier)","schema":{"type":"string","pattern":"^[a-f\\d]{24}$","default":"000000000000000000000000"}}],"responses":{"200":{"description":"Consent record retrieved successfully","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentReadResponse"}}}},"400":{"description":"Invalid request parameters","content":{"text/plain":{"examples":{"invalid-params":{"value":"Error while getting params [requestId: abc123]"}}}}},"404":{"description":"Consent not found","content":{"text/plain":{"examples":{"not-found":{"value":"Consent not found [requestId: abc123]"}}}}},"500":{"description":"Database error","content":{"text/plain":{"examples":{"db-error":{"value":"Error while reading consent in DB [requestId: def456]"}}}}}}},"options":{"tags":["CORS"],"summary":"CORS preflight for single consent reading","responses":{"200":{"description":"CORS preflight response","headers":{"Access-Control-Allow-Origin":{"schema":{"type":"string"}},"Access-Control-Allow-Methods":{"schema":{"type":"string"}},"Access-Control-Allow-Headers":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"ConsentSubmissionRequest":{"type":"object","required":["accept","token","preferences"],"properties":{"_id":{"oneOf":[{"type":"string","pattern":"^[a-f\\d]{24}$","description":"MongoDB ObjectId"},{"type":"string","format":"uuid","description":"UUID v7 for improved sorting and indexing"}],"description":"Unique identifier for the consent record"},"accept":{"type":"boolean","description":"Whether the user accepted or denied consent"},"token":{"type":"string","minLength":1,"maxLength":255,"description":"User token for consent tracking"},"value":{"oneOf":[{"type":"string","minLength":1,"maxLength":255},{"type":"null"}],"description":"Optional consent value"},"timestamp":{"type":"string","format":"date-time","description":"ISO timestamp when consent was given (defaults to server time)"},"headers":{"type":"object","description":"Mobile-specific request headers and metadata","properties":{"user-agent":{"type":"string"},"x-mobile-platform":{"type":"string","enum":["ios","android","react-native","flutter","xamarin","cordova"]},"x-mobile-version":{"type":"string"},"x-app-version":{"type":"string"},"x-device-id":{"type":"string"},"x-session-id":{"type":"string"}}},"preferences":{"type":"object","description":"Detailed consent preferences and mobile-specific data","properties":{"vendors":{"type":"object","description":"Vendor-specific consent preferences","additionalProperties":{"type":"boolean"}},"config":{"type":"object","properties":{"name":{"type":"string"},"identifier":{"type":"string"},"language":{"type":"string"},"consentMode":{"type":"string"},"displayMode":{"type":"string"},"tag":{"type":"string"},"mobileContext":{"type":"object","properties":{"platform":{"type":"string"},"offline":{"type":"boolean"},"syncBatch":{"type":"string"},"networkType":{"type":"string","enum":["wifi","cellular","unknown"]}}}}},"googleConsentMode":{"type":"object","properties":{"version":{"type":"number"},"analytics_storage":{"type":"string","enum":["granted","denied"]},"ad_storage":{"type":"string","enum":["granted","denied"]},"ad_user_data":{"type":"string","enum":["granted","denied"]},"ad_personalization":{"type":"string","enum":["granted","denied"]},"functionality_storage":{"type":"string","enum":["granted","denied"]},"personalization_storage":{"type":"string","enum":["granted","denied"]},"security_storage":{"type":"string","enum":["granted","denied"]}}},"consentFor":{"type":"string"},"version":{"type":"number"},"tcString":{"type":"string","minLength":1,"description":"IAB TCF v2.x consent string (base64url, may include a `.publisherTC` segment). When present, the gateway runs the TCF validation pipeline against this body: JSON-Schema (`tcf-consent.schema.json`), decoder (`workers/api-gateway/tcf/tc-decoder.js`), CMP-id gate (rejects `cmpId === 0` / `cmpVersion === 0`), and GVL-membership gate against the TCF_GVL KV namespace."},"cmpVersion":{"type":"integer","minimum":1,"description":"CMP implementation version that produced the TC string. Optional; ignored when `tcString` is absent."},"gdprApplies":{"type":"boolean","description":"Whether GDPR applies for the user. Optional; ignored when `tcString` is absent."}},"additionalProperties":true}}},"ConsentSubmissionResponse":{"type":"object","properties":{"consentId":{"type":"string","description":"Generated consent UUID"},"_id":{"type":"string"},"projectId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"accept":{"type":"boolean"},"collection":{"type":"string"},"identifier":{"type":"string"},"token":{"type":"string"},"value":{"type":"string"},"preferences":{"type":"object"}}},"ConsentBatchRequest":{"type":"object","required":["consents"],"properties":{"batchId":{"type":"string","description":"Unique identifier for this batch"},"syncContext":{"type":"object","properties":{"platform":{"type":"string"},"appVersion":{"type":"string"},"offlineDuration":{"type":"number"},"networkQuality":{"type":"string"}}},"consents":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","required":["clientId","collection","configId","consent"],"properties":{"clientId":{"type":"string","pattern":"^[a-f\\d]{24}$"},"collection":{"type":"string","description":"Service/collection type. `terms` is the public alias for `contractsV2` (Terms & Conditions).","enum":["processings","contracts","contractsV2","cookies","terms"]},"configId":{"type":"string"},"consent":{"$ref":"#/components/schemas/ConsentSubmissionRequest"}}}}}},"ConsentBatchResponse":{"type":"object","properties":{"batchId":{"type":"string"},"processed":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"status":{"type":"string","enum":["success","error"]},"consentId":{"type":"string"},"error":{"type":"string"}}}}}},"ConsentReadResponse":{"oneOf":[{"type":"object","properties":{"accept":{"type":"boolean","const":false}},"description":"No consent found"},{"type":"object","properties":{"_id":{"type":"string"},"projectId":{"type":"string"},"token":{"type":"string"},"collection":{"type":"string"},"identifier":{"type":"string"},"accept":{"type":"boolean"},"timestamp":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"headers":{"type":"object"},"value":{"type":"string"},"preferences":{"type":"object","properties":{"vendors":{"type":"object"},"config":{"type":"object"},"googleConsentMode":{"type":"object"},"mobileOptimized":{"type":"object","properties":{"compactVendors":{"type":"array"},"essentialOnly":{"type":"boolean"}}},"tcString":{"type":"string","description":"Stored IAB TCF v2.x consent string. Present only on records that were submitted with `preferences.tcString` set on the POST body."},"cmpVersion":{"type":"integer"},"gdprApplies":{"type":"boolean"}}},"decoded":{"$ref":"#/components/schemas/TCDecodedResponse","description":"Decoded view of the stored `preferences.tcString`. Attached by the consent-read decorator only when (a) `preferences.tcString` is a non-empty string and (b) the decoder succeeds. Absent on non-TCF records and on records whose stored TC string fails to decode (the upstream payload bytes are otherwise forwarded unchanged)."}}}]},"HealthCheckResponse":{"type":"object","properties":{"status":{"type":"string","enum":["healthy","degraded"]},"timestamp":{"type":"string","format":"date-time"},"services":{"type":"object","properties":{"database":{"type":"string","enum":["up","down","fallback"]},"kinesis":{"type":"string","enum":["up","down"]},"s3_fallback":{"type":"string","enum":["available","unavailable"]}}},"circuit_breaker":{"type":"boolean"},"disaster_recovery":{"type":"boolean"}}},"ConfigurationDiscoveryResponse":{"type":"object","required":["projectId","primaryColor","configurations"],"properties":{"projectId":{"type":"string","description":"Project identifier"},"primaryColor":{"type":"string","nullable":true,"description":"Project primary/accent colour as a hex string (e.g. '#a90626'), for styling buttons, toggles and active UI elements. Project-level — does not vary per configuration. Source: 'themes.light.colors.primary' (Back-Office theme editor) when set; otherwise the legacy 'client.colors.main'. 'null' only when neither is a valid hex colour."},"configurations":{"type":"array","description":"Available consent configurations","items":{"$ref":"#/components/schemas/ConfigurationSummary"}},"defaultConfigId":{"type":"string","description":"Default configuration identifier to use"}}},"ConfigurationSummary":{"type":"object","required":["identifier","name"],"properties":{"identifier":{"type":"string","description":"Configuration identifier (MongoDB ObjectId)"},"name":{"type":"string","description":"Configuration name"},"language":{"type":"string","description":"Configuration language code (e.g., 'en', 'fr')"},"country":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}],"description":"Country or region code, e.g. 'EU' or 'US'. May be a list (e.g. ['PL','CZ','SK']) when one config targets multiple countries (ENG-12343); the value is passed through verbatim."},"title":{"type":"string","description":"Display title for the configuration"},"isDefault":{"type":"boolean","description":"Whether this is the default configuration"},"flowType":{"type":"string","enum":["brands","tcf"],"description":"Consent flow type. 'brands' entries are sourced from client.axept.io/{projectId}.json (cookies[]). 'tcf' entries are sourced from the separate client.axept.io/tcf/{projectId}.json upstream (consents[]). Native SDKs use this to route to the correct UI flow without a second call. A project may host entries from both flows; they are returned in a single array with stable order (brands first, then tcf)."},"cmpVersion":{"type":"integer","description":"TCF CMP version (from upstream `cmp_version`). Present only when flowType === 'tcf'."},"googleConsentMode":{"type":"object","description":"Google Consent Mode settings for this TCF configuration. Present only when flowType === 'tcf'.","properties":{"adsDataRedaction":{"type":"boolean","description":"Whether ads_data_redaction is enabled."},"urlPassthrough":{"type":"boolean","description":"Whether url_passthrough is enabled."}}},"googleAdditionalConsent":{"type":"boolean","description":"Whether Google Additional Consent mode is enabled. Present only when flowType === 'tcf'."}}},"ErrorResponse":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string","description":"snake_case error code. Most JSON error responses on this route are emitted via `shared/response-utils.js#createErrorResponse` (which sets `timestamp` unconditionally), but a few paths build the JSON body directly without it — notably `proxyToBackend`'s catch returning `backend_unavailable` 502. The `ErrorResponse` shape is reused across all of them, so the set of codes is not fixed per endpoint. On POST `/mobile/consents/{clientId}/{collection}/{configId}`, observed codes include — TCF-specific (from `validateTcfConsentBody`): `invalid_argument`, `bad_format`, `unsupported_version`, `invalid_cmp`, `unknown_gvl_version`; non-TCF gateway validation (from `validateConsentSubmission`): `invalid_configuration`; gateway-level: `payload_too_large` (request-body size cap enforced in `worker.js` before validation runs), `internal_error`, `configuration_error` (backend URL env var missing OR corrupted project auth data), `internal_server_error`, `backend_unavailable` (502), `storage_unavailable` (503 from `AUTHORIZATION_KV` read failure in `validateConsentSubmission`)."},"message":{"type":"string","description":"Human-readable error message"},"timestamp":{"type":"string","format":"date-time","description":"ISO timestamp the error was produced at. Set unconditionally by `shared/response-utils.js#createErrorResponse`, which covers most JSON error responses on this route — but a handful of paths build the JSON body directly (e.g. the `proxyToBackend` catch returning `backend_unavailable` 502), so the field is not strictly guaranteed and therefore not part of `required`."},"details":{"type":"array","description":"Per-field validation errors when `error === \"invalid_argument\"`. Shape matches `formatValidationErrors` in `workers/api-gateway/schemas/validator.js` (AJV errors normalized into stable `{ field, message, constraint }` objects).","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"},"constraint":{"type":"string"}}}}}},"MobileChangelog":{"type":"object","description":"Curated, API-behavior-level changelog for SDK integrators. Served at GET /mobile/changelog; also embedded in this spec as the `x-changelog` extension. Newest first.","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/ChangelogEntry"},"description":"Changelog entries, newest first."}}},"ChangelogEntry":{"type":"object","properties":{"id":{"type":"string","description":"Stable slug for the change.","example":"eng-12029-categories-scope"},"date":{"type":"string","format":"date","example":"2026-07-07"},"version":{"type":"string","description":"Release version the change shipped in.","example":"0.18.0"},"endpoint":{"type":"string","description":"Full public path affected (may contain {param} templates), or \"*\" for a global change.","example":"/mobile/vendors/{projectId}/categories"},"type":{"type":"string","enum":["breaking","feature","fix","deprecation"],"example":"breaking"},"summary":{"type":"string","description":"What changed."},"migration":{"type":"string","nullable":true,"description":"How to adapt; typically present for breaking/deprecation entries."},"sunset":{"type":"string","format":"date","nullable":true,"description":"Removal date; only for `deprecation` entries — drives the RFC 8594 Sunset header."}}},"TermsContent":{"type":"object","description":"Normalized Terms & Conditions content returned by GET /mobile/terms/{projectId}/{configId}. The backend collection is `contractsV2`; `terms` is the public alias.","required":["projectId","identifier","language","tag","content"],"properties":{"projectId":{"type":"string","description":"Project identifier (MongoDB ObjectId)","example":"67fcdb2b52ab9a99a5865f4d"},"identifier":{"type":"string","description":"Configuration identifier","example":"terms_of_sale"},"language":{"type":"string","description":"BCP 47 language code of the content","example":"fr"},"tag":{"type":"string","description":"Version tag of the content that was served","example":"latest"},"versionName":{"type":"string","nullable":true,"description":"Human-readable version label","example":"v1.2"},"publishedAt":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 publication timestamp","example":"2026-04-01T00:00:00.000Z"},"pdfUrl":{"type":"string","nullable":true,"description":"URL to the PDF version of the document. Use GET /mobile/terms/{projectId}/{configId}/pdf to stream it.","example":"https://storage.axeptio.eu/terms/67fcdb2b52ab9a99a5865f4d/terms_of_sale/v1.2.pdf"},"content":{"type":"object","description":"Structured document content","required":["strings","sections"],"properties":{"strings":{"type":"object","description":"Key/value map of UI label strings (e.g. title, button labels)","additionalProperties":{"type":"string"},"example":{"title":"Conditions Générales de Vente","acceptButton":"J'accepte","declineButton":"Je refuse"}},"sections":{"type":"array","description":"Ordered list of document sections","items":{"type":"object","properties":{"id":{"type":"string","description":"Section identifier","example":"section-1"},"title":{"type":"string","description":"Section heading","example":"Objet"},"body":{"type":"string","description":"Section body text (may contain HTML)","example":"Les présentes conditions régissent l'utilisation du service."}}}}}}}},"TcfTextsResponse":{"type":"object","description":"Combined UI label + GVL reference texts for the requested locale.","properties":{"lang":{"type":"string","description":"Echo of the requested BCP47 locale","example":"fr-CA"},"gvlVersion":{"type":"integer","description":"GVL version the reference texts were projected from","example":348},"uiFallback":{"type":"string","description":"Present only when the locale's UI pack was unavailable and English labels were served instead","example":"en"},"texts":{"type":"object","description":"Keyed text maps. `ui` holds the UI label strings; the remaining keys hold GVL reference texts as `{ id, name, description }` maps.","required":["ui"],"properties":{"ui":{"type":"object","additionalProperties":{"type":"string"},"description":"UI label strings keyed by name (e.g. `acceptAllButton`). Placeholders like `{0}` are preserved for client-side interpolation."},"purposes":{"$ref":"#/components/schemas/TcfTextCategory"},"specialPurposes":{"$ref":"#/components/schemas/TcfTextCategory"},"features":{"$ref":"#/components/schemas/TcfTextCategory"},"specialFeatures":{"$ref":"#/components/schemas/TcfTextCategory"},"stacks":{"$ref":"#/components/schemas/TcfTextCategory"},"dataCategories":{"$ref":"#/components/schemas/TcfTextCategory"}}}},"required":["lang","gvlVersion","texts"]},"TcfTextCategory":{"type":"object","description":"Map of GVL entry id → localized `{ id, name, description }`.","additionalProperties":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string"}}}},"MobileProjectConfig":{"type":"object","description":"Unified mobile project configuration returned by GET /mobile/configurations/{projectId}. Merges the Brands and TCF upstreams into a single discriminated configurations[] array plus project-level metadata.","required":["projectId","isDomainRestrictionEnabled","verifiedDomains","configurations","screens"],"properties":{"projectId":{"type":"string","description":"Project identifier (MongoDB ObjectId)"},"primaryColor":{"type":"string","nullable":true,"description":"Project primary colour (hex) for styling buttons / toggles / active UI elements; null when no upstream colour is configured (ENG-12028)."},"isDomainRestrictionEnabled":{"type":"boolean","description":"Whether the SDK should enforce its domain restriction gate. Always present (default false) (ENG-12074)."},"verifiedDomains":{"type":"array","items":{"type":"string"},"description":"Operator-verified domains for the restriction gate. Always present (default []) (ENG-12074)."},"configurations":{"type":"array","items":{"$ref":"#/components/schemas/MobileConfigurationEntry"},"description":"Discriminated list of Brands and TCF configurations available for this project."},"defaultConfigId":{"type":"string","nullable":true,"description":"Identifier of the default configuration (the entry whose isDefault is true); null when the project has no configurations."},"screens":{"$ref":"#/components/schemas/NativeScreens"}}},"NativeScreens":{"type":"object","description":"Hardcoded content for the native SDK's supplementary screens (ENG-12817). Not configurable from the back office; titles/descriptions are localized via the `lang` query parameter with silent English fallback.","required":["att","permissions"],"properties":{"att":{"$ref":"#/components/schemas/NativeScreen"},"permissions":{"$ref":"#/components/schemas/NativeScreen"}}},"NativeScreen":{"type":"object","description":"A single supplementary screen's static content.","required":["background","media","title","description"],"properties":{"background":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Background gradient as two hex colours.","example":["#E9CBFF","#FFCCCD"]},"media":{"type":"string","nullable":true,"description":"Illustration URL; currently always null (pending MSK-232)."},"title":{"type":"string","description":"Localized screen title."},"description":{"type":"string","description":"Localized screen description."}}},"MobileConfigurationEntry":{"type":"object","description":"A single configuration within configurations[]. flowType discriminates the Brands vs TCF shape; the TCF-only fields and the googleConsentMode shape vary accordingly.","required":["identifier","name","language","country","title","flowType","isDefault","policyUrl","cookieStatementUrl"],"properties":{"identifier":{"type":"string","description":"Configuration identifier (MongoDB ObjectId)"},"name":{"type":"string","description":"Configuration name"},"language":{"type":"string","description":"Primary language (BCP47-ish); defaults to \"en\"."},"country":{"type":"string","description":"Country label; defaults to \"Unknown\"."},"title":{"type":"string","description":"Display title; defaults to the configuration name."},"flowType":{"type":"string","enum":["brands","tcf"],"description":"Source flow. \"brands\" entries originate from the Brands upstream; \"tcf\" entries from the TCF upstream."},"isDefault":{"type":"boolean","description":"True for the entry referenced by defaultConfigId."},"googleConsentMode":{"description":"Optional Google Consent Mode block. Strict-shape (all-or-nothing): omitted when the upstream block is incomplete. The shape depends on flowType — Brands carries the 5-field operator-configured block (BrandsGoogleConsentMode); TCF carries the 2-field block (TcfGoogleConsentMode).","oneOf":[{"$ref":"#/components/schemas/BrandsGoogleConsentMode"},{"$ref":"#/components/schemas/TcfGoogleConsentMode"}]},"cmpVersion":{"type":"integer","description":"TCF-only. CMP version surfaced on flowType=\"tcf\" entries; omitted otherwise."},"googleAdditionalConsent":{"type":"boolean","description":"TCF-only. Google Additional Consent flag on flowType=\"tcf\" entries; omitted otherwise."},"askNewConsent":{"type":"boolean","description":"Whether to ask for consent again when the configuration changes (ENG-13864). Both flows: Brands from `settings.askNewConsent` (re-prompt when a configured vendor is missing from the stored choice); TCF from `ask_new_consent` (re-prompt when `cmpVersion` differs from the stored TC string's cmpVersion). Omitted when unset or not a boolean."},"purposes":{"type":"array","items":{"type":"integer"},"description":"TCF-only. Publisher-selected IAB purpose ids (from upstream `purposes`, ENG-13864); may be empty when the publisher selects stacks instead. Omitted on Brands entries and when any element is not an integer."},"stacks":{"type":"array","items":{"type":"integer"},"description":"TCF-only. Publisher-selected IAB stack ids (from upstream `stacks`, ENG-13864); resolve each stack's purposes via the GVL. Omitted on Brands entries and when any element is not an integer."},"expirationTtlDays":{"type":"integer","minimum":1,"maximum":190,"description":"TCF-only. Consent storage lifetime in days (from upstream `expiration_ttl_days`, ENG-13864). Omitted when unset or outside 1..190; the web CMP then uses its 190-day default."},"policyUrl":{"type":"string","nullable":true,"description":"Operator-set Privacy Policy URL for this configuration (ENG-12795). Sourced from the Brands welcome step (`policyUrl`) or the TCF consent root (`privacy_policy_url`); always present, `null` when absent or not an absolute http(s) URL."},"cookieStatementUrl":{"type":"string","nullable":true,"description":"Operator-set Cookie Statement URL for this configuration (ENG-12795). Sourced from the Brands welcome step (`cookieStatementUrl`) or the TCF consent root (`cookie_statement_url`); always present, `null` when absent or not an absolute http(s) URL."}}},"BrandsGoogleConsentMode":{"type":"object","description":"Google Consent Mode block for Brands (flowType=\"brands\") configurations (ENG-12054). All five fields are present together.","additionalProperties":false,"required":["display","displayIllustrations","position","adsDataRedaction","urlPassthrough"],"properties":{"display":{"type":"boolean","description":"Whether to render the GCM consent step."},"displayIllustrations":{"type":"boolean","description":"Whether to show GCM illustrations."},"position":{"type":"string","description":"Position of the GCM step in the flow (e.g. \"second\")."},"adsDataRedaction":{"type":"boolean","description":"Google ads_data_redaction signal."},"urlPassthrough":{"type":"boolean","description":"Google url_passthrough signal."}}},"TcfGoogleConsentMode":{"type":"object","description":"Google Consent Mode block for TCF (flowType=\"tcf\") configurations (ENG-12027). Carries only the two Google signals.","additionalProperties":false,"required":["adsDataRedaction","urlPassthrough"],"properties":{"adsDataRedaction":{"type":"boolean","description":"Google ads_data_redaction signal."},"urlPassthrough":{"type":"boolean","description":"Google url_passthrough signal."}}},"TcfStandardInformationResponse":{"type":"object","description":"TCF standard-information notice for native UIs, returned by GET /mobile/tcf/standard-info/{projectId}/{configId}. The introduction text has all dynamic placeholders resolved server-side; `variables` exposes the same pre-calculated values discretely; `cards` are the ordered GVL sections to render.","required":["lang","gvlVersion","variables","introduction","cards"],"properties":{"lang":{"type":"string","description":"Resolved BCP47 language tag the payload was localized to — normalized to lowercase (e.g. `?lang=FR` → `fr`); \"en\" when none requested.","example":"fr"},"gvlVersion":{"type":"integer","description":"IAB GVL vendor-list version the cards/counts were built from.","example":123},"variables":{"type":"object","description":"Pre-calculated values interpolated into the introduction. Clients should NOT recompute these (MSK-213 addendum).","required":["vendorCount","refuseOrWithdrawButtonText","chooseButtonText","manageCookiesText","onThisAppOrWebsite","ourAppOrWebsite"],"properties":{"vendorCount":{"type":"integer","description":"Number of configured IAB vendors present in the GVL.","example":42},"refuseOrWithdrawButtonText":{"type":"string","description":"Label resolved for the refuse/withdraw action (publisher deny label, or the continue-without-consent label when that mode is enabled).","example":"Continue without consent"},"chooseButtonText":{"type":"string","description":"Label for the \"let me choose\" action.","example":"Let me choose"},"manageCookiesText":{"type":"string","description":"Resolved phrase describing where to manage cookies (app button / website link wording per `context`).","example":"the \"Manage cookies\" button in your application preferences settings"},"onThisAppOrWebsite":{"type":"string","description":"Contextual phrase (\"On this application\" / \"On this website\").","example":"On this application"},"ourAppOrWebsite":{"type":"string","description":"Contextual phrase (\"our application\" / \"our website\").","example":"our application"}}},"introduction":{"type":"object","description":"The publisher's standard-information notice, localized to the requested `lang`: the text is sourced from the project's banner whose language matches the primary subtag of `lang` (region ignored — e.g. `fr-CA` matches a `fr` banner), within the same regulation grouping (country/subdivision) as the requested `configId`. When no such banner exists, it falls back to the requested configuration's own introduction text. `html` is `raw` with every `[...]` placeholder resolved; both are null when the publisher configured no introduction.","required":["raw","html"],"properties":{"raw":{"type":"string","nullable":true,"description":"Publisher introduction HTML, verbatim (placeholders intact).","example":"We work with [VENDOR_COUNT] partners."},"html":{"type":"string","nullable":true,"description":"Introduction HTML with all placeholders interpolated server-side.","example":"We work with 42 partners."}}},"cards":{"type":"array","description":"Ordered sections to render as cards: with stacks configured, purpose 1 leads followed by one card per stack; otherwise the purposes / special purposes / features / special features lists.","items":{"type":"object","required":["id","type","title","description","vendorCount"],"properties":{"id":{"type":"string","description":"Stable card id, e.g. `purpose-1`, `stack-7`, `feature-2`.","example":"purpose-1"},"type":{"type":"string","enum":["purpose","specialPurpose","feature","specialFeature","stack"],"description":"Kind of GVL section this card represents."},"title":{"type":"string","description":"Localized section name.","example":"Store and/or access information on a device"},"description":{"type":"string","description":"Localized section description.","example":"Cookies, device identifiers, or other information can be stored or accessed on your device for the purposes presented to you."},"vendorCount":{"type":"integer","description":"Number of configured vendors covering this section.","example":30}}}}}},"TcfConfigurationResponse":{"type":"object","description":"Mobile-optimized TCF GVL projection for a project configuration, returned by GET /mobile/tcf/configurations/{projectId}/{configId}.","required":["projectId","configId","lang","welcomingScreen","gvlSpecificationVersion","vendorListVersion","tcfPolicyVersion","purposes","specialPurposes","features","specialFeatures","stacks","vendors","publisherRestrictions","builtAt"],"properties":{"projectId":{"type":"string","description":"Project identifier (echoed from the path)."},"configId":{"type":"string","description":"Configuration identifier (echoed from the path)."},"lang":{"type":"string","description":"Resolved language tag (lowercased; \"en\" when none requested)."},"welcomingScreen":{"type":"object","description":"Welcoming screen for the configuration (ENG-12948). `title`, `subtitle` (the consent-row `subtitle`, falling back to the `surtitle` overline), and `picture` (the config `logo`) come from the requested configuration's consent-row root. `description` is the product-authored, POEditor-localized string, resolved for the requested `?lang=` (then its primary subtag, then English). Object for TCF configs; null only for metadata-less rows.","nullable":true,"allOf":[{"$ref":"#/components/schemas/WelcomingScreen"}]},"gvlSpecificationVersion":{"type":"string","description":"GVL specification version."},"vendorListVersion":{"type":"integer","description":"IAB Global Vendor List version."},"tcfPolicyVersion":{"type":"integer","description":"TCF policy version."},"gvlLastUpdated":{"type":"string","nullable":true,"description":"Timestamp of the last GVL update, or null when unknown."},"purposes":{"type":"object","additionalProperties":true,"description":"Localized TCF purposes, keyed by purpose id."},"specialPurposes":{"type":"object","additionalProperties":true,"description":"Localized TCF special purposes, keyed by id."},"features":{"type":"object","additionalProperties":true,"description":"Localized TCF features, keyed by id."},"specialFeatures":{"type":"object","additionalProperties":true,"description":"Localized TCF special features, keyed by id."},"stacks":{"type":"object","additionalProperties":true,"description":"Localized TCF stacks, keyed by id."},"vendors":{"type":"array","items":{"$ref":"#/components/schemas/TcfVendor"},"description":"Projected vendor list."},"publisherRestrictions":{"type":"array","description":"Publisher restrictions applied to vendors.","items":{"type":"object","required":["purposeId","restrictionType","vendorIds"],"properties":{"purposeId":{"type":"integer","description":"Purpose the restriction applies to."},"restrictionType":{"type":"integer","description":"IAB restriction type (0=not allowed, 1=require consent, 2=require legitimate interest)."},"vendorIds":{"type":"array","items":{"type":"integer"},"description":"Vendor ids the restriction applies to."}}}},"builtAt":{"type":"string","format":"date-time","description":"ISO-8601 timestamp when the projection was built."}}},"TcfVendor":{"type":"object","description":"A single vendor in the TCF projection.","required":["id","name"],"properties":{"id":{"type":"integer","description":"IAB vendor id."},"name":{"type":"string","description":"Vendor name."},"purposes":{"type":"array","items":{"type":"integer"},"description":"Purpose ids declared on a consent legal basis."},"legIntPurposes":{"type":"array","items":{"type":"integer"},"description":"Purpose ids declared on a legitimate-interest legal basis."},"flexiblePurposes":{"type":"array","items":{"type":"integer"},"description":"Purpose ids the vendor declares as flexible."},"specialPurposes":{"type":"array","items":{"type":"integer"},"description":"Special purpose ids."},"features":{"type":"array","items":{"type":"integer"},"description":"Feature ids."},"specialFeatures":{"type":"array","items":{"type":"integer"},"description":"Special feature ids."},"policyUrl":{"type":"string","description":"Vendor privacy policy URL."},"axeptio":{"type":"object","nullable":true,"additionalProperties":true,"description":"Axeptio-specific vendor metadata, or null when absent."}}},"ConsentBatchRequest1":{"type":"object","properties":{"projectId":{"type":"string","description":"Project identifier"},"consents":{"type":"array","items":{"$ref":"#/components/schemas/MobileConsentRecord"},"description":"Array of consent records to submit"}}},"MobileConsentRecord":{"type":"object","properties":{"userToken":{"type":"string","description":"Unique user identifier"},"timestamp":{"type":"string","format":"date-time","description":"Consent collection timestamp"},"preferences":{"type":"object","additionalProperties":{"type":"boolean"},"description":"User consent preferences by category"},"metadata":{"$ref":"#/components/schemas/MobileMetadata"}}},"MobileMetadata":{"type":"object","properties":{"platform":{"type":"string","enum":["ios","android","react-native","flutter"],"description":"Mobile platform"},"appVersion":{"type":"string","description":"Application version"},"sdkVersion":{"type":"string","description":"SDK version used"},"deviceId":{"type":"string","description":"Anonymous device identifier"},"locale":{"type":"string","description":"User locale (e.g., en-US)"}}},"ConsentBatchResponse1":{"type":"object","properties":{"processed":{"type":"integer","description":"Number of successfully processed records"},"failed":{"type":"integer","description":"Number of failed records"},"results":{"type":"array","items":{"$ref":"#/components/schemas/BatchResult"}}}},"BatchResult":{"type":"object","properties":{"userToken":{"type":"string","description":"User token from request"},"status":{"type":"string","enum":["success","error"],"description":"Processing status"},"error":{"type":"string","description":"Error message if status is error"}}},"MobileConsentState":{"type":"object","properties":{"userToken":{"type":"string","description":"User identifier"},"projectId":{"type":"string","description":"Project identifier"},"preferences":{"type":"object","additionalProperties":{"type":"boolean"},"description":"Current consent preferences"},"lastUpdated":{"type":"string","format":"date-time","description":"Last update timestamp"},"version":{"type":"integer","description":"Consent record version"}}},"ConsentTokenResponse":{"type":"object","required":["token"],"properties":{"token":{"type":"string","description":"Cryptographically secure random consent token","example":"abc123def456ghi789"}}},"AuthValidationResponse":{"type":"object","required":["valid"],"properties":{"valid":{"type":"boolean","description":"Whether the bearer token is valid","example":true},"projectId":{"type":"string","description":"Project ID associated with the token (if valid)","example":"67fcdb2b52ab9a99a5865f4d"},"tier":{"type":"string","description":"Customer tier associated with the project","enum":["free","pro","enterprise"],"example":"enterprise"}}},"VendorsResponse":{"type":"object","properties":{"projectId":{"type":"string","description":"Project identifier","example":"67fcdb2b52ab9a99a5865f4d"},"vendors":{"type":"array","items":{"$ref":"#/components/schemas/Vendor"},"description":"List of vendors for the project"},"totalVendors":{"type":"integer","description":"Total number of unique vendors","example":5},"categories":{"type":"array","items":{"type":"string"},"description":"All available vendor categories","example":["Fonctionnels","Analytics","Marketing"]},"fetchedAt":{"type":"string","format":"date-time","description":"Timestamp when data was fetched","example":"2025-09-12T14:30:00.000Z"}}},"ConfigurationVendorsResponse":{"type":"object","properties":{"projectId":{"type":"string","description":"Project identifier","example":"67fcdb2b52ab9a99a5865f4d"},"configId":{"type":"string","description":"Configuration identifier","example":"67fcddc2673e895f81feb6c3"},"configName":{"type":"string","description":"Human-readable configuration name","example":"fr-eu-14-04-2025"},"vendors":{"type":"array","items":{"$ref":"#/components/schemas/Vendor"},"description":"List of vendors for the specific configuration"},"totalVendors":{"type":"integer","description":"Total number of vendors in this configuration","example":3},"categories":{"type":"array","items":{"type":"string"},"description":"Vendor categories present in this configuration","example":["Fonctionnels","Analytics"]},"fetchedAt":{"type":"string","format":"date-time","description":"Timestamp when data was fetched","example":"2025-09-12T14:30:00.000Z"}}},"VendorCategoriesResponse":{"type":"object","description":"Vendors grouped by category for a project. Categories are sourced from a single selected configuration's purpose steps (same enriched shape as the with-configId endpoint), so each category carries a canonical `category` slug (stable key) and a localized `name`/`title` label plus `subtitle`/`description`/`image`. On this endpoint `name` is the localized label (backward compatible); prefer `category` + `title`. The optional `lang` query parameter selects which single-language configuration answers; the resolved `configId`/`language` are echoed. Projects with no Brands configuration (e.g. TCF-only) fall back to legacy free-text grouping — `category` is null and items carry only `name`/`vendors`/`count`, and `configId`/`configName`/`language`/`welcomingScreen` are omitted.","properties":{"projectId":{"type":"string","description":"Project identifier","example":"67fcdb2b52ab9a99a5865f4d"},"configId":{"type":"string","description":"Identifier of the configuration selected to answer (per `lang`, else the project default, else the first). Omitted on the legacy free-text fallback.","example":"67fcddc2673e895f81feb6c3"},"configName":{"type":"string","description":"Human-readable name of the selected configuration. Omitted on the legacy fallback.","example":"fr-eu-14-04-2025"},"language":{"type":"string","nullable":true,"description":"Language of the selected configuration (e.g. `fr`); null when the configuration carries none. Omitted on the legacy fallback.","example":"fr"},"welcomingScreen":{"type":"object","description":"Welcoming screen data for the selected configuration (same shape as the with-configId endpoint). Omitted on the legacy fallback.","nullable":true,"allOf":[{"$ref":"#/components/schemas/WelcomingScreen"}]},"categories":{"type":"array","items":{"$ref":"#/components/schemas/ConfigVendorCategory"},"description":"Vendors organized by category, enriched with step metadata (title, subtitle, description, image). On the legacy fallback each item carries only name/vendors/count."},"totalCategories":{"type":"integer","description":"Total number of categories","example":4},"totalVendors":{"type":"integer","description":"Total number of vendors across all categories","example":12},"fetchedAt":{"type":"string","format":"date-time","description":"Timestamp when data was fetched","example":"2025-09-12T14:30:00.000Z"}}},"ConfigurationVendorCategoriesResponse":{"type":"object","properties":{"projectId":{"type":"string","description":"Project identifier","example":"67fcdb2b52ab9a99a5865f4d"},"configId":{"type":"string","description":"Configuration identifier","example":"67fcddc2673e895f81feb6c3"},"configName":{"type":"string","description":"Human-readable configuration name","example":"fr-eu-14-04-2025"},"welcomingScreen":{"type":"object","description":"Welcoming screen data. For Brands configs, sourced from the configuration's first non-vendor step (title, subtitle, description, picture). For TCF/Publisher configs, populated from the consent-row root: `title`, `subtitle` (the upstream `subtitle` field, falling back to the `surtitle` overline when absent), and `picture` (the config `logo`, when set); `description` is a product-authored, language-localized string (ENG-12737). Null for metadata-less configurations.","nullable":true,"allOf":[{"$ref":"#/components/schemas/WelcomingScreen"}]},"categories":{"type":"array","items":{"$ref":"#/components/schemas/ConfigVendorCategory"},"description":"Configuration vendors organized by category, enriched with step metadata (title, subtitle, description, image)"},"totalCategories":{"type":"integer","description":"Total number of categories in this configuration","example":1},"totalVendors":{"type":"integer","description":"Total number of vendors across all categories in this configuration","example":1},"fetchedAt":{"type":"string","format":"date-time","description":"Timestamp when data was fetched","example":"2025-09-22T10:30:00.000Z"}}},"WelcomingScreen":{"type":"object","nullable":true,"description":"Welcoming screen data. For Brands configs it is sourced from the configuration's first non-vendor step. For TCF/Publisher configs it is sourced from the consent-row root (title, subtitle, picture) plus a product-authored, language-localized description.","properties":{"title":{"type":"string","nullable":true,"description":"Welcoming screen title, localized to the configuration's language.","example":"Votre vie privée, nos engagements"},"subtitle":{"type":"string","nullable":true,"description":"Welcoming screen subtitle, localized to the configuration's language.","example":"Quelques instants pour choisir"},"description":{"type":"string","nullable":true,"description":"Welcoming screen description, localized to the configuration's language. For Brands configs this is the welcome step's plain text (HTML stripped). For TCF/Publisher configs this is a product-authored hardcoded string localized off the consent row's language, with an English fallback.","example":"Nous utilisons des technologies pour vous offrir une meilleure expérience."},"picture":{"type":"string","nullable":true,"description":"Absolute URL of the welcoming screen illustration. Bundled default-illustration slugs are not resolvable server-side and are returned as null.","example":"https://axeptio.imgix.net/2025/04/welcome-illustration.png"}}},"Vendor":{"type":"object","properties":{"id":{"type":"string","description":"Unique vendor identifier (Axeptio internal id)","example":"62fd02d250c4539d3456e350"},"iabId":{"type":"integer","nullable":true,"description":"IAB TCF Global Vendor List (GVL) vendor id when the vendor is IAB-registered; null otherwise.","example":755},"name":{"type":"string","description":"Vendor technical name","example":"googletagmanager"},"title":{"type":"string","description":"Vendor display name","example":"Google Tag Manager"},"description":{"type":"string","description":"Short vendor description","example":"Système de gestion de balisage"},"longDescription":{"type":"string","description":"Detailed vendor description","example":"Mise en place d'un système de balisage pour traiter plus facilement les informations liées aux données."},"category":{"type":"string","description":"Primary vendor category","example":"Fonctionnels, Gestion des étiquettes"},"categories":{"type":"array","items":{"type":"string"},"description":"All vendor categories","example":["Fonctionnels","Gestion des étiquettes"]},"type":{"type":"string","description":"Vendor type classification","example":"functional, tag_management"},"domain":{"type":"string","description":"Vendor domain/website","example":"https://tagmanager.google.com/"},"policyUrl":{"type":"string","description":"Vendor privacy policy URL","example":"https://marketingplatform.google.com/about/analytics/tag-manager/use-policy/"},"isAxeptioVendor":{"type":"boolean","description":"Whether this is an Axeptio-managed vendor","example":true},"image":{"$ref":"#/components/schemas/VendorImage"}}},"VendorImage":{"type":"object","properties":{"identifier":{"type":"string","description":"Image identifier","example":"googletagmanager"},"baseUrl":{"type":"string","description":"Base imgix URL for the vendor image","example":"https://axeptio.imgix.net/vendors/googletagmanager"},"optimized":{"$ref":"#/components/schemas/OptimizedImages"},"fallbackUrl":{"type":"string","description":"Fallback favicon URL","example":"https://favicons.axept.io/favicons?domain=tagmanager.google.com","nullable":true}}},"OptimizedImages":{"type":"object","properties":{"small":{"type":"string","description":"32x32px optimized image for mobile list views","example":"https://axeptio.imgix.net/vendors/googletagmanager?w=32&h=32&fit=crop&auto=format,compress"},"medium":{"type":"string","description":"64x64px optimized image for mobile detail views","example":"https://axeptio.imgix.net/vendors/googletagmanager?w=64&h=64&fit=crop&auto=format,compress"},"large":{"type":"string","description":"128x128px optimized image for high-DPI displays","example":"https://axeptio.imgix.net/vendors/googletagmanager?w=128&h=128&fit=crop&auto=format,compress"}}},"VendorCategory":{"type":"object","properties":{"name":{"type":"string","description":"Category name","example":"Fonctionnels"},"vendors":{"type":"array","items":{"$ref":"#/components/schemas/Vendor"},"description":"Vendors in this category"},"count":{"type":"integer","description":"Number of vendors in this category","example":3}}},"ConfigVendorCategory":{"type":"object","description":"A configuration category. For Brands configs each category is a purpose step. `category` is the canonical, language-independent slug (the stable key clients should use); `title` is the localized display label. `name` is a legacy field kept for backward compatibility — on `/mobile/vendors/{projectId}/categories` it is the localized label, on `/mobile/vendors/{projectId}/{configId}/categories` it is the slug; prefer `category` + `title` over `name`. For configs without purpose steps (e.g. TCF) `category` is null, `name` is a free-text vendor category, and the metadata fields are null.","properties":{"category":{"type":"string","nullable":true,"description":"Canonical, language-independent purpose slug (e.g. `analytics`, `targeted_ads`, `social_networks`) — the stable key. Null for configs without purpose steps (free-text grouping).","example":"analytics"},"name":{"type":"string","description":"Legacy label/key. Localized display label on the project-wide endpoint; the canonical slug on the with-configId endpoint. Prefer `category` (slug) and `title` (label).","example":"analytics"},"title":{"type":"string","nullable":true,"description":"Localized category title from the step template; null when unavailable.","example":"Statistiques et audience"},"subtitle":{"type":"string","nullable":true,"description":"Localized category subtitle from the step template; null when unavailable.","example":"Compter nos calories, c'est important !"},"description":{"type":"string","nullable":true,"description":"Localized category description, derived from the step message with HTML stripped to plain text; null when unavailable.","example":"Nous sommes chargés de compter le nombre de visites sur ce site."},"image":{"type":"string","nullable":true,"description":"Absolute URL of the category illustration when the customer set a custom image. Bundled default-illustration slugs are not resolvable server-side and are returned as null.","example":"https://axeptio.imgix.net/2025/04/analytics-illustration.png"},"vendors":{"type":"array","items":{"$ref":"#/components/schemas/Vendor"},"description":"Vendors in this category"},"count":{"type":"integer","description":"Number of vendors in this category","example":1}}},"HeadlessEvent":{"type":"object","description":"Event object for headless CMP analytics. The `projectId` and `source` fields are enriched server-side and must NOT be set by the client — everything else below is required by the events backend.","required":["type","timestamp","token","domain","config","preferences","userAgent","pathname","referrer"],"properties":{"type":{"type":"string","description":"Event type — only these 9 values are accepted for headless clients","enum":["page_view","consent_displayed","consent_accepted","consent_rejected","consent_partial","settings_opened","category_toggled","app_att_authorized","app_att_denied"],"example":"consent_displayed"},"timestamp":{"type":"string","format":"date-time","description":"Event timestamp in ISO 8601 format","example":"2025-09-18T10:30:00.000Z"},"token":{"type":"string","minLength":10,"maxLength":100,"description":"Session token identifying the user session (same token returned by GET /mobile/token)","example":"a3b7x9k2m5p8q1w4"},"domain":{"type":"string","minLength":1,"description":"Domain where the event occurred","example":"example.com"},"config":{"type":"object","description":"The CMP configuration active when the event occurred. `identifier` (the configId) is required; `name`/`language`/`consentMode`/`displayMode` are accepted for richer telemetry but not enforced.","required":["identifier"],"properties":{"identifier":{"type":"string","description":"The active configuration's identifier (configId)","example":"6859079473219bcbb8435079"},"name":{"type":"string","example":"Default"},"language":{"type":"string","example":"en"},"consentMode":{"type":"string","example":"opt-in"},"displayMode":{"type":"string","example":"popup"}},"example":{"identifier":"6859079473219bcbb8435079","name":"Default","language":"en","consentMode":"opt-in","displayMode":"popup"}},"preferences":{"type":"object","description":"Category/vendor slugs the user has enabled and disabled as of this event","required":["enabled","disabled"],"properties":{"enabled":{"type":"array","items":{"type":"string","minLength":1},"example":["essential","analytics"]},"disabled":{"type":"array","items":{"type":"string","minLength":1},"example":["marketing"]}},"example":{"enabled":["essential"],"disabled":["marketing"]}},"userAgent":{"type":"string","minLength":1,"description":"Client user agent string","example":"AxeptioSDKExample/1 CFNetwork/3860.500.112 Darwin/25.4.0"},"pathname":{"type":"string","minLength":1,"description":"App screen/route where the event occurred. Use \"/\" for native clients without a real path.","example":"/"},"referrer":{"type":["string","null"],"minLength":1,"description":"Referring URL, or null when not applicable (e.g. native apps)","example":null}},"additionalProperties":true},"HeadlessEventValidationError":{"type":"object","description":"Validation error response returned when one or more events in the batch fail schema validation. All events are validated atomically — if any fails, the entire batch is rejected.","properties":{"error":{"type":"string","enum":["validation_error","invalid_format"],"description":"Error code"},"message":{"type":"string","description":"Human-readable error message (first validation error)"},"timestamp":{"type":"string","format":"date-time","description":"Server timestamp when the error occurred"},"details":{"type":"array","description":"Per-event validation errors (only present for validation_error)","items":{"type":"object","properties":{"eventIndex":{"type":"integer","description":"Zero-based index of the event in the submitted array"},"errors":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","description":"Field that failed validation"},"message":{"type":"string","description":"Validation error message"},"constraint":{"type":"string","description":"Validation constraint that was violated (e.g., enum, required, minLength)"},"allowedValues":{"type":"array","items":{"type":"string"},"description":"Allowed values (for enum constraints)"},"receivedValue":{"description":"The value that was received"}}}}}}},"allowedEventTypes":{"type":"array","items":{"type":"string"},"description":"List of all allowed event type values","example":["page_view","consent_displayed","consent_accepted","consent_rejected","consent_partial","settings_opened","category_toggled","app_att_authorized","app_att_denied"]}}},"MobileEvent":{"type":"object","description":"Generic mobile event schema (non-headless clients). For headless clients, use HeadlessEvent instead.","required":["type","timestamp","metadata"],"properties":{"type":{"type":"string","description":"Event type identifier","enum":["consent_displayed","consent_accepted","consent_rejected","consent_partial","page_view","widget_opened","widget_closed","vendor_toggled","category_toggled","settings_opened","privacy_policy_clicked","pagehide","pagevisible"],"example":"consent_displayed"},"timestamp":{"type":"string","format":"date-time","description":"Event timestamp in ISO 8601 format","example":"2025-09-18T10:30:00.000Z"},"metadata":{"$ref":"#/components/schemas/EventMetadata"},"projectId":{"type":"string","description":"Project identifier (optional if included in metadata)","example":"67fcdb2b52ab9a99a5865f4d"},"correlationId":{"type":"string","description":"Correlation ID for tracking related events","example":"consent-flow-abc123"},"step":{"type":"object","description":"Consent step information (for consent-related events)","properties":{"name":{"type":"string","example":"cookies"},"identifier":{"type":"string","example":"step_001"}}},"deployment":{"type":"string","description":"Deployment timestamp for configuration version tracking","example":"2025-09-18T08:00:00.000Z"},"source":{"type":"string","description":"Event source identifier","enum":["sdk-app","sdk-web","sdk-app-brands"],"example":"sdk-app"}}},"EventMetadata":{"type":"object","properties":{"source":{"type":"string","description":"Event source","example":"sdk-app"},"platform":{"type":"string","description":"Mobile platform","enum":["ios","android","react-native","flutter","web"],"example":"ios"},"appVersion":{"type":"string","description":"Application version","example":"1.2.3"},"sdkVersion":{"type":"string","description":"SDK version","example":"2.1.0"},"deviceInfo":{"type":"object","description":"Device information (optional)","properties":{"model":{"type":"string","example":"iPhone14,2"},"os":{"type":"string","example":"iOS 16.0"},"screenSize":{"type":"string","example":"390x844"}}},"categories":{"type":"array","items":{"type":"string"},"description":"Consent categories (for consent events)","example":["analytics","marketing","functional"]},"context":{"type":"string","description":"Special context flags for event classification","enum":["eventSentBeforeConfigLoaded","configIsEmpty","freeTrialBroken","projectDeleted","cookiesConfigEmpty"],"example":"eventSentBeforeConfigLoaded"}}},"EventSubmissionResponse":{"type":"object","properties":{"success":{"type":"boolean","description":"Whether the events were successfully processed","example":true},"processed":{"type":"integer","description":"Number of events processed","example":2},"timestamp":{"type":"string","format":"date-time","description":"Processing timestamp","example":"2025-09-18T10:30:16.000Z"},"batchId":{"type":"string","description":"Unique identifier for the batch submission","example":"batch-abc123-def456"},"errors":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer","description":"Index of the event that caused the error"},"error":{"type":"string","description":"Error code"},"message":{"type":"string","description":"Error message"}}},"description":"Array of errors for failed events (only present if some events failed)"}}},"ConsentSubmissionRequest1":{"type":"object","required":["userToken","preferences"],"properties":{"userToken":{"type":"string","description":"Unique identifier for the user providing consent","example":"user-abc123"},"timestamp":{"type":"string","format":"date-time","description":"Timestamp when consent was provided (ISO 8601 format). If not provided, server will use current time.","example":"2025-09-18T10:30:00.000Z"},"preferences":{"type":"object","additionalProperties":{"type":"boolean"},"description":"User consent preferences by category (e.g., analytics: true, marketing: false)","example":{"analytics":true,"marketing":false,"functional":true}},"metadata":{"allOf":[{"$ref":"#/components/schemas/MobileMetadata"}],"description":"Additional metadata about the consent submission context"},"source":{"type":"string","description":"Source of the consent submission","enum":["sdk-app","sdk-web","sdk-app-brands"],"example":"sdk-app"},"ipAddress":{"type":"string","description":"IP address of the user (optional, for compliance tracking)","example":"192.168.1.100"},"userAgent":{"type":"string","description":"User agent string (optional, for compliance tracking)","example":"MyApp/1.2.3 (iOS 16.0; iPhone14,2)"}}},"ConsentSubmissionResponse1":{"type":"object","properties":{"success":{"type":"boolean","description":"Whether the consent submission was successful","example":true},"consentId":{"type":"string","description":"Unique identifier for the created consent record","example":"consent-abc123-def456"},"timestamp":{"type":"string","format":"date-time","description":"Server timestamp when the consent was processed","example":"2025-09-18T10:30:01.000Z"},"message":{"type":"string","description":"Human-readable success message","example":"Consent record created successfully"},"version":{"type":"integer","description":"Version number of the consent record (for tracking changes)","example":1}}},"StatsResponse":{"type":"object","description":"Aggregated consent statistics for a project (DAT-2060).","properties":{"success":{"type":"boolean","example":true},"meta":{"$ref":"#/components/schemas/StatsMeta"},"summary":{"$ref":"#/components/schemas/StatsMetrics"},"data":{"type":"array","items":{"$ref":"#/components/schemas/StatsRow"}},"status":{"type":"integer","example":200}}},"StatsMeta":{"type":"object","description":"Applied filters and result metadata.","properties":{"filters":{"type":"object","properties":{"from":{"type":"string","format":"date","nullable":true,"example":"2026-01-01"},"to":{"type":"string","format":"date","nullable":true,"example":"2026-01-31"}}},"groupBy":{"type":"array","items":{"type":"string"},"example":["date"]},"rows":{"type":"integer","nullable":true,"example":31},"timeZone":{"type":"string","example":"UTC"}}},"StatsMetrics":{"type":"object","description":"Consent metrics. Counts are integers; the *Rate fields are ratios in the 0..1 range.","properties":{"pageview":{"type":"integer"},"visitor":{"type":"integer"},"couldInteract":{"type":"integer"},"widgetDisplay":{"type":"integer"},"interaction":{"type":"integer"},"consent":{"type":"integer"},"reject":{"type":"integer"},"partial":{"type":"integer"},"bounce":{"type":"integer"},"quickBounce":{"type":"integer"},"suspectedBots":{"type":"integer"},"interactionRate":{"type":"number"},"consentRate":{"type":"number"},"optInRate":{"type":"number"},"bounceRate":{"type":"number"},"quickBounceRate":{"type":"number"},"responses":{"$ref":"#/components/schemas/StatsResponseBreakdown"}}},"PublicGeolocationResponse":{"type":"object","description":"Which flow and config a visitor at this location should be served.","required":["flowType","configId","matchMode","isFallback"],"properties":{"flowType":{"type":"string","enum":["tcf","brands"],"example":"tcf","description":"Which banner to load. Drives nothing else — the config id below already belongs to this flow."},"configId":{"type":"string","description":"The config to load. Maps to `axeptioSettings.cookiesVersion` in the web SDK.","example":"6904ca84683c996cd3788342"},"matchMode":{"type":"string","enum":["country-exact","subdivision","regulation","fallback"],"example":"country-exact","description":"How precisely the config matched, most specific first. `fallback` means the project's `geolocationFallbackConfigId` answered rather than any geo rule — worth surfacing in diagnostics, since it usually means the visitor's country was never targeted."},"isFallback":{"type":"boolean","description":"Convenience mirror of `matchMode === \"fallback\"`.","example":false},"language":{"type":"string","nullable":true,"description":"The resolved config's language, when it declares one."},"geolocation":{"type":"object","properties":{"countryCode":{"type":"string","description":"ISO 3166-1 alpha-2, or `Unknown` when the lookup failed. Cloudflare sentinels (`XX`, `T1` for Tor) are treated as failures rather than reported as countries.","example":"FR"},"subdivisionCode":{"type":"string","nullable":true,"example":"QC"},"source":{"type":"string","enum":["cloudflare","override","none"],"example":"cloudflare","description":"`override` only occurs outside production."}}},"regulation":{"type":"object","properties":{"code":{"type":"string","example":"gdpr"},"internalIsoCode":{"type":"string","nullable":true,"example":"EU"},"isDefault":{"type":"boolean","example":false,"description":"True when the governance table had no entry for this location and the default (`gdpr`) was assumed."}}}}},"StatsResponseBreakdown":{"type":"array","description":"The `consent`, `partial` and `reject` counts pivoted into rows. Same numbers, different shape: a BI tool charts a pie by grouping on a dimension, and three separate columns give it nothing to group on. Always exactly three entries, always in the order Consent, Partial, Reject; a period with no data yields three zero entries rather than an absent field. The labels are a stable contract — they surface as dimension values inside customer reports.","minItems":3,"maxItems":3,"items":{"type":"object","required":["label","value"],"properties":{"label":{"type":"string","enum":["Consent","Partial","Reject"]},"value":{"type":"integer"}}},"example":[{"label":"Consent","value":42656},{"label":"Partial","value":34260},{"label":"Reject","value":40567}]},"StatsRow":{"description":"One aggregation row: all metrics plus the dimension keys for the requested groupBy.","allOf":[{"$ref":"#/components/schemas/StatsMetrics"},{"type":"object","properties":{"date":{"type":"string","format":"date","example":"2026-01-01"},"country":{"type":"string","example":"FR"},"device":{"type":"string","example":"mobile"},"configVersion":{"type":"string"}}}]},"ErrorResponse1":{"type":"object","properties":{"error":{"type":"string","description":"Error code","example":"unauthorized"},"message":{"type":"string","description":"Human-readable error message","example":"Missing or invalid authorization header"}}},"GeolocationResponse":{"type":"object","required":["geolocation","regulation","resolvedConfig","next","availableRegulations"],"properties":{"geolocation":{"type":"object","required":["countryCode","source"],"properties":{"countryCode":{"type":"string","description":"ISO 3166-1 alpha-2 country code, or 'Unknown' when geo detection failed","example":"FR"},"subdivisionCode":{"type":["string","null"],"description":"ISO 3166-2 subdivision code without the country prefix (e.g. 'QC' for Quebec)","example":"IDF"},"source":{"type":"string","enum":["cloudflare","override","none"],"description":"How the geolocation was determined. 'override' means a non-production query-param override was honored."}}},"regulation":{"type":"object","required":["code","isDefault"],"properties":{"code":{"type":"string","description":"Regulation identifier — gdpr, uk_gdpr, fadp, ccpa, loi25.","example":"gdpr"},"internalIsoCode":{"type":["string","null"],"description":"Internal bucket code used by Axeptio BO/API (e.g. 'EU', 'GB', 'US', 'CA-QC')","example":"EU"},"isDefault":{"type":"boolean","description":"True when no regulation mapped to the visitor's country and the endpoint defaulted to GDPR"}}},"resolvedConfig":{"type":"object","required":["configId","matchMode","isFallback"],"properties":{"configId":{"type":"string","description":"Cookies configuration identifier to use for this visitor"},"matchMode":{"type":"string","enum":["country-exact","regulation","subdivision","fallback"],"description":"Which resolution pass produced the configId"},"language":{"type":["string","null"],"description":"Language of the resolved config"},"platform":{"type":["string","null"],"description":"Platform tag of the resolved config, when the config declares one"},"isFallback":{"type":"boolean","description":"True when the geolocationFallbackConfigId path was used"},"platformNarrowed":{"type":"boolean","description":"True when a platform-specific config was found and preferred over platform-agnostic candidates"}}},"next":{"type":"object","properties":{"vendorsEndpoint":{"type":"string","description":"Path the caller should GET next to fetch the vendor list for the resolved config","example":"/mobile/vendors/67fcdb2b52ab9a99a5865f4d/6859079473219bcbb8435079"}}},"availableRegulations":{"type":"array","items":{"type":"string"},"description":"Full list of regulation identifiers known to the governance table","example":["gdpr","uk_gdpr","fadp","ccpa","loi25"]}}},"TcfEncodeRequest":{"type":"object","description":"TCModel-shape input for POST /mobile/tcf/encode. Mirrors the decoded shape produced by /mobile/tcf/decode: arrays of integer ids, camelCase keys. `cmpId` and `cmpVersion` are required; `vendorListVersion` is required and selects the GVL used to encode (the GVL also fixes the encoded `tcfPolicyVersion`). Omitted signal blocks default to empty.","required":["cmpId","cmpVersion","vendorListVersion"],"properties":{"cmpId":{"type":"integer","description":"Registered IAB CMP id (must be > 1; 0/1 are reserved and rejected as `bad_format`).","example":260},"cmpVersion":{"type":"integer","description":"CMP version (>= 0).","example":1},"vendorListVersion":{"type":"integer","description":"IAB GVL vendorListVersion to encode against. Must be a version cached by the worker (see GET /mobile/tcf/gvl/latest); unknown versions return 400 `unknown_gvl_version`.","example":348},"consentLanguage":{"type":"string","description":"Two-letter consent language. Defaults to \"EN\" when omitted.","example":"EN"},"publisherCC":{"type":"string","description":"Two-letter ISO-3166 publisher country code. Defaults to \"AA\" when omitted.","example":"FR"},"purposes":{"type":"object","description":"Global purpose bitfields. Omitted arrays default to empty.","properties":{"consents":{"type":"array","items":{"type":"integer"},"example":[1,2,3]},"legitimateInterests":{"type":"array","items":{"type":"integer"},"example":[2,7]}}},"vendors":{"type":"object","description":"Per-vendor consent / legitimate-interest ids.","properties":{"consents":{"type":"array","items":{"type":"integer"},"example":[8,99]},"legitimateInterests":{"type":"array","items":{"type":"integer"}}}},"specialFeatures":{"type":"array","items":{"type":"integer"},"description":"Asserted special-feature opt-in ids.","example":[1]},"publisher":{"type":"object","description":"Publisher-side bitfields encoded into the publisher TC segment.","properties":{"consents":{"type":"array","items":{"type":"integer"}},"legitimateInterests":{"type":"array","items":{"type":"integer"}},"customConsents":{"type":"array","items":{"type":"integer"}},"customLegitimateInterests":{"type":"array","items":{"type":"integer"}}}}}},"TcfEncodeResponse":{"type":"object","required":["tcString"],"properties":{"tcString":{"type":"string","description":"Spec-compliant base64url IAB TCF v2.x consent string. Round-trips through GET /mobile/tcf/decode back to the submitted signals.","example":"COtybn4OtybnEAAABAENAA"}}},"TCDecodedResponse":{"type":"object","description":"JSON-serializable view of the decoded TCModel produced by workers/api-gateway/tcf/tc-decoder.js. Vectors are flattened into sorted arrays of integer ids; timestamps are ISO-8601 strings. Empty bitfields are returned as empty arrays (`[]`), not omitted.","properties":{"cmpId":{"type":"integer","example":7},"cmpVersion":{"type":"integer","example":1},"vendorListVersion":{"type":"integer","example":348},"tcfPolicyVersion":{"type":"integer","example":4},"publisherCC":{"type":"string","description":"Two-letter ISO-3166 publisher country code encoded in the TC string","example":"FR"},"consentLanguage":{"type":"string","example":"EN"},"created":{"type":["string","null"],"format":"date-time","description":"ISO-8601 creation timestamp encoded in the TC string, or `null` if absent on the decoded model"},"lastUpdated":{"type":["string","null"],"format":"date-time"},"purposes":{"type":"object","description":"Sorted lists of asserted purpose ids decoded from the global-purposes bitfields.","required":["consents","legitimateInterests"],"properties":{"consents":{"type":"array","items":{"type":"integer"},"example":[1,2,3]},"legitimateInterests":{"type":"array","items":{"type":"integer"},"example":[2,7]}}},"vendors":{"type":"object","description":"Sorted lists of vendor ids decoded from the per-vendor consent / LI bitfields.","required":["consents","legitimateInterests"],"properties":{"consents":{"type":"array","items":{"type":"integer"}},"legitimateInterests":{"type":"array","items":{"type":"integer"}}}},"specialFeatures":{"type":"array","items":{"type":"integer"},"description":"Asserted special-feature ids (1..2 per TCF v2.2)","example":[1]},"specialPurposes":{"type":"array","items":{"type":"integer"},"description":"Reserved for future use; currently always returned as an empty array — the TC string does not encode publisher-side special purposes."},"publisher":{"type":"object","description":"Publisher segment (`.publisherTC`) — empty arrays when the segment is absent.","required":["consents","legitimateInterests","customConsents","customLegitimateInterests"],"properties":{"consents":{"type":"array","items":{"type":"integer"}},"legitimateInterests":{"type":"array","items":{"type":"integer"}},"customConsents":{"type":"array","items":{"type":"integer"}},"customLegitimateInterests":{"type":"array","items":{"type":"integer"}}}}}},"GVLResponse":{"type":"object","description":"Full IAB Global Vendor List snapshot as published by IAB Europe (https://vendor-list.consensu.org/v3/vendor-list.json). Only top-level shape is documented here — the structure follows the upstream GVL JSON schema verbatim.","properties":{"gvlSpecificationVersion":{"type":"integer","example":3},"vendorListVersion":{"type":"integer","example":348},"tcfPolicyVersion":{"type":"integer","example":4},"lastUpdated":{"type":"string","format":"date-time"},"purposes":{"type":"object","description":"Map of purposeId → {id, name, description, descriptionLegal, ...}"},"specialPurposes":{"type":"object"},"features":{"type":"object"},"specialFeatures":{"type":"object"},"stacks":{"type":"object"},"dataCategories":{"type":"object"},"vendors":{"type":"object","description":"Map of vendorId → vendor object (name, purposes, legIntPurposes, flexiblePurposes, specialPurposes, features, specialFeatures, urls, dataDeclaration, ...)"}}},"GVLLangResponse":{"type":"object","description":"Translated subset of a GVL version. The shape mirrors the upstream IAB per-language file (`purposes-{lang}.json`): the localized `purposes` / `specialPurposes` / `features` / `specialFeatures` / `stacks` / `dataCategories` maps (each id → name/description), plus `vendorListVersion` and `lastUpdated`. The `vendors` map is intentionally absent — vendor names are language-agnostic. English (`en`) is served from the base GVL snapshot and has this exact same shape. (The upstream file does NOT carry `gvlSpecificationVersion`, `tcfPolicyVersion`, or a `language` field — use `/mobile/tcf/gvl/latest` for those.)","properties":{"vendorListVersion":{"type":"integer","example":348},"lastUpdated":{"type":"string","format":"date-time","example":"2026-05-29T00:00:00Z"},"purposes":{"type":"object"},"specialPurposes":{"type":"object"},"features":{"type":"object"},"specialFeatures":{"type":"object"},"stacks":{"type":"object"},"dataCategories":{"type":"object"}}},"GVLStatusResponse":{"type":"object","description":"Diagnostic snapshot of the GVL cache returned by GET /mobile/tcf/gvl/status.","required":["ok","latest","cachedVersions","count"],"properties":{"ok":{"type":"boolean","description":"True when a `gvl:latest` pointer exists (the pipeline has seeded KV). False signals the GVL refresh has not run yet.","example":true},"latest":{"type":"object","nullable":true,"description":"The current latest-version pointer, or null when KV is not seeded yet.","properties":{"version":{"type":"integer","example":348},"fetchedAt":{"type":"string","format":"date-time","example":"2026-05-30T06:00:00.000Z"},"etag":{"type":"string","nullable":true,"example":"W/\"abc123\""},"lastModified":{"type":"string","nullable":true,"example":"Sat, 30 May 2026 06:00:00 GMT"}}},"cachedVersions":{"type":"array","description":"Archived GVL versions currently cached in KV, sorted ascending.","items":{"type":"integer"},"example":[347,348]},"count":{"type":"integer","description":"Number of cached versions (length of cachedVersions).","example":2}}}},"headers":{"CacheControl":{"description":"Response cacheability directive. TCF endpoints return `public, max-age=300`.","schema":{"type":"string","example":"public, max-age=300"}},"RateLimitLimit":{"description":"Per-project request budget (shared across all of a project's tokens) for the current rolling window","schema":{"type":"integer","example":100}},"RateLimitRemaining":{"description":"Requests remaining in the current rolling window before throttling kicks in","schema":{"type":"integer","example":99}},"RateLimitReset":{"description":"Absolute Unix timestamp (milliseconds since epoch) at which the current rate-limit window resets. NOTE: not a delta-seconds value — clients should diff against `Date.now()` to compute a wait time.","schema":{"type":"integer","format":"int64","example":1747044720000}},"RetryAfter":{"description":"Seconds until the rate limiter will accept the next request (only set on 429 responses).","schema":{"type":"integer","example":42}}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"opaque","description":"Headless SDK project bearer token. An opaque credential — not a JWT, despite what this scheme previously claimed — that identifies exactly one project; the API resolves the project from the token, so no project parameter is needed."},"fronteggAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Frontegg access token, for common-API consumers (BI tools) that should not have to hold a headless SDK credential. It carries an identity but no project, so endpoints authenticated this way require an explicit projectId and authorize access to it per request. Currently accepted on GET /stats only."}}},"servers":[{"url":"https://headless-api.axeptio.tech","description":"Production environment"},{"url":"https://staging-api.axeptio.tech","description":"Staging environment"}],"x-changelog":[{"id":"eng-13480-gvl-mirror-403","date":"2026-10-01","version":"0.25.1","endpoint":"/mobile/tcf/gvl/{version}","type":"fix","summary":"Archived GVL versions and unpublished GVL languages that the mirror doesn't hold return `404 not_found` again. Since 2026-09-15 they returned `502` because the mirror's storage now reports a missing file as access-denied (HTTP 403) rather than not-found. Every 403 from the mirror is now treated as not-found, so a genuine access-denied error on these files would also appear as 404; other mirror failures (network errors, other 4xx, 5xx) still return `502`.","migration":"No action required. A client that retried these 502s will now get the documented 404 immediately."},{"id":"eng-13864-configurations-sdk-parity","date":"2026-10-01","version":"0.25.0","endpoint":"/mobile/configurations/{projectId}","type":"feature","summary":"Configuration entries now carry the fields the web CMPs use to decide when to ask again and which TCF purposes to show. `askNewConsent` (boolean) is on both flows. TCF entries also carry the publisher-selected `purposes` and `stacks` (integer arrays) and `expirationTtlDays` (integer, 1..190). Every field is optional and omitted when the configuration doesn't set it or the upstream value is malformed; values are never coerced. `expirationTtlDays` is omitted outside 1..190, where the web falls back to its 190-day default.","migration":"No action required — the fields are additive and optional, so existing SDKs ignore them. To match web behaviour: re-prompt when `askNewConsent` is true and (Brands) a configured vendor is missing from the stored choice, or (TCF) `cmpVersion` differs from the stored TC string's cmpVersion; build the TCF purposes list from `purposes` plus the purposes of each stack in `stacks` (resolved via the GVL); use `expirationTtlDays`, defaulting to 190 when absent."},{"id":"sup-1027-stats-credential-503","date":"2026-09-03","version":"0.24.0","endpoint":"/stats","type":"fix","summary":"When the stats backend rejects our credentials, `GET /stats` now returns `503 service_unavailable` instead of `502 bad_gateway`. Previously every upstream failure — a revoked credential, a broken query, a network-policy block — collapsed into one indistinguishable 502, which is why a three-week outage went unnoticed. 502 now means a query or runtime failure specifically; 503 means the backend is unavailable to us, whether unconfigured or refusing our credentials.","migration":"No action required unless you branch on the exact status. A client that treats all 5xx as retryable is unaffected. If you alert on `/stats` failures, 503 is now the signal that the problem is on our side and a retry will not help until we act; 502 remains worth retrying."},{"id":"sup-1027-stats-frontegg-auth","date":"2026-08-10","version":"0.23.0","endpoint":"/stats","type":"feature","summary":"`GET /stats` now also accepts a Frontegg access token, so BI/common-API consumers no longer need a headless SDK bearer token. A Frontegg-authenticated call must name its project with `?projectId=`, and access to it is authorized per request. Existing headless bearer tokens keep working unchanged.","migration":"No action required today — this is additive and the headless bearer token remains supported for now. To move a BI connector across: authenticate with a Frontegg access token and add `?projectId=<24-hex>` to the request. A missing or malformed `projectId` on a Frontegg call is a 400; a project the identity may not read is a 403. On a headless token `projectId` is optional, but naming a project other than the token's own is a 403 rather than a silent fallback."},{"id":"eng-12948-tcf-configurations-welcomingscreen","date":"2026-07-09","version":"0.19.0","endpoint":"/mobile/tcf/configurations/{projectId}/{configId}","type":"feature","summary":"Added a `welcomingScreen` block (title, subtitle, picture from the consent-row root; product-authored localized `description`) so a TCF integration can build its consent UI from a single call instead of also calling the categories endpoint. The `description` follows the request `?lang=` (then its primary subtag, then English)."},{"id":"eng-12029-categories-scope","date":"2026-07-07","version":"0.18.0","endpoint":"/mobile/vendors/{projectId}/categories","type":"breaking","summary":"The project-wide categories endpoint now returns a single selected configuration's purpose-step categories instead of the union across all configurations, and `name` is now a localized display label (previously a free-text category label).","migration":"Use the new `category` field (canonical, language-independent slug) as the stable key and `title` for the display label. The flat vendor list `GET /mobile/vendors/{projectId}` still returns the full union across configurations. Pass `?lang=` to select a language."},{"id":"eng-12029-category-slug-and-lang","date":"2026-07-07","version":"0.18.0","endpoint":"/mobile/vendors/{projectId}/categories","type":"feature","summary":"Categories are enriched with a canonical `category` slug plus `title`, `subtitle`, `description`, `image`, and a `welcomingScreen`, and accept an optional `?lang=` selector (best-effort, silent fallback, never 400). The `category` slug is also added (additively) to the with-configId categories endpoint."},{"id":"eng-12817-native-screens","date":"2026-07-07","version":"0.18.0","endpoint":"/mobile/configurations/{projectId}","type":"feature","summary":"Added a `screens` object (hardcoded ATT + Permissions native-SDK screen content: background gradient, media, title, description) and an optional `?lang=` selector with a silent English fallback."},{"id":"eng-12737-tcf-welcome-description","date":"2026-07-07","version":"0.18.0","endpoint":"/mobile/vendors/{projectId}/{configId}/categories","type":"feature","summary":"Region-aware TCF `welcomingScreen.description` on the with-configId categories endpoint, resolved from the stored config language/country (`${language}-${country}` → `${language}` → `en`)."}]}