{"openapi":"3.1.0","info":{"title":"KEKSONAUT API","version":"0.6.0","description":"Cookie Consent Management API. Note: KEKSONAUT does not implement IAB TCF or Google CMP certification."},"servers":[{"url":"/"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"},"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Public API key (`cm_live_…`), created under /api/api-keys. May alternatively be sent as `Authorization: Bearer cm_live_…`. Rate limited to 120 requests/minute per key."}},"schemas":{"DpaDocumentReference":{"type":"object","additionalProperties":false,"properties":{"version":{"type":"string"},"documentHash":{"type":"string","pattern":"^[a-f0-9]{64}$"}},"required":["version","documentHash"]},"DpaIncorporation":{"type":"object","description":"Immutable contract-inclusion record. A legacy migration is an administrative record at recordedAt, not a customer's historical acceptance or signature.","properties":{"id":{"type":"string"},"source":{"type":"string","enum":["signup_terms","existing_customer_migration","organization_creation"]},"recordedAt":{"type":"string","format":"date-time"},"version":{"type":"string"},"hash":{"type":"string"},"documentText":{"type":"string"},"legalVersion":{"type":"string"},"actorUserId":{"type":"string"},"migrationId":{"type":"string"},"legalEvidence":{"type":"object","description":"Optional unchanged earlier Terms receipt; not re-labelled as acceptance of the current DPA","properties":{"version":{"type":"string"},"acceptedAt":{"type":"string","format":"date-time"},"source":{"type":"string"}}}},"required":["id","source","recordedAt","version","hash","documentText","legalVersion"]},"DpaAcceptanceInput":{"type":"object","additionalProperties":false,"properties":{"version":{"type":"string","minLength":1,"maxLength":100},"documentHash":{"type":"string","pattern":"^[a-f0-9]{64}$"},"accepted":{"type":"boolean","const":true},"authorized":{"type":"boolean","const":true},"signatoryName":{"type":"string","minLength":2,"maxLength":200},"controller":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string"},"address":{"type":"string"},"email":{"type":"string","format":"email"}},"required":["name","address","email"]}},"required":["version","documentHash","accepted","authorized","signatoryName","controller"]},"PublicDpaDocument":{"allOf":[{"$ref":"#/components/schemas/DpaDocument"},{"type":"object","properties":{"required":{"type":"boolean","description":"Hosted DPA applies; false on independent self-hosted installations"},"legalVersion":{"type":"string","description":"Current Terms/Privacy revision for the signup acceptance"}},"required":["required","legalVersion"]}]},"DpaDocument":{"type":"object","description":"Complete versioned German Art. 28 contract including all three annexes. An unverified operator profile produces ready=false and cannot be accepted.","properties":{"title":{"type":"string"},"version":{"type":"string"},"hash":{"type":"string","pattern":"^[a-f0-9]{64}$"},"text":{"type":"string","description":"UTF-8 Markdown; hash is SHA-256 of this exact text"},"ready":{"type":"boolean"},"configurationError":{"type":["string","null"]}},"required":["title","version","hash","text","ready","configurationError"]},"DpaAcceptance":{"type":"object","description":"Immutable organization acceptance evidence. Previous versions remain available until organization deletion.","properties":{"id":{"type":"string"},"version":{"type":"string"},"hash":{"type":"string"},"documentText":{"type":"string"},"organizationName":{"type":"string"},"acceptedAt":{"type":"string","format":"date-time"},"acceptedByUserId":{"type":"string"},"acceptedByName":{"type":"string"},"acceptedByEmail":{"type":"string","format":"email"},"acceptedByRole":{"type":"string","enum":["owner","admin"]},"signatoryName":{"type":"string"},"controller":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string"},"address":{"type":"string"},"email":{"type":"string","format":"email"}},"required":["name","address","email"]}},"required":["id","version","hash","documentText","organizationName","acceptedAt","acceptedByUserId","acceptedByName","acceptedByEmail","acceptedByRole","signatoryName","controller"]},"DpaStatus":{"type":"object","properties":{"required":{"type":"boolean"},"applicable":{"type":"boolean","description":"False on independent self-hosted installations"},"canAccept":{"type":"boolean","description":"Hosted mode, verified document and owner/admin role"},"document":{"$ref":"#/components/schemas/DpaDocument"},"acceptance":{"oneOf":[{"$ref":"#/components/schemas/DpaAcceptance"},{"type":"null"}]},"incorporation":{"oneOf":[{"$ref":"#/components/schemas/DpaIncorporation"},{"type":"null"}]}},"required":["required","applicable","canAccept","document","acceptance","incorporation"]},"ConsentEvidence":{"type":"object","properties":{"eventId":{"type":"string"},"websiteId":{"type":"string"},"occurredAt":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["accept_all","save","update"]},"version":{"type":"integer"},"bannerVersionId":{"type":["string","null"]},"host":{"type":"string"},"originVerified":{"type":"boolean"},"categories":{"type":"object","additionalProperties":{"type":"boolean"}},"services":{"type":"object","additionalProperties":{"type":"boolean"}},"ipHash":{"type":["string","null"],"description":"128-bit website-keyed pseudonymized IP hash"},"userAgent":{"type":"string"}},"required":["eventId","websiteId","occurredAt","type","version","bannerVersionId","host","originVerified","categories","services","ipHash","userAgent"]},"TwoFactorStatus":{"type":"object","properties":{"enabled":{"type":"boolean"},"enabledAt":{"type":["string","null"],"format":"date-time"},"recoveryCodesRemaining":{"type":"integer","minimum":0,"maximum":10},"recoveryCodesGeneratedAt":{"type":["string","null"],"format":"date-time"},"hasPassword":{"type":"boolean","description":"False for Google/Microsoft-only accounts: switching 2FA off then needs a sign-in within the last 10 minutes instead of the password."}},"required":["enabled","enabledAt","recoveryCodesRemaining","recoveryCodesGeneratedAt","hasPassword"]},"TwoFactorChallenge":{"type":"object","description":"The first sign-in step succeeded; no session yet. Exchange the challenge with a code at POST /api/auth/2fa/verify within expiresIn seconds.","properties":{"twoFactorRequired":{"type":"boolean","enum":[true]},"challengeToken":{"type":"string"},"expiresIn":{"type":"integer","enum":[300]}},"required":["twoFactorRequired","challengeToken","expiresIn"]},"TrustedDevice":{"type":"object","description":"A browser trusted for 30 days after the second factor (cookie cm_trusted_device).","properties":{"id":{"type":"string"},"label":{"type":"string","description":"Browser and operating system from the user agent, e.g. \"Firefox · Windows\"; empty when not recognised."},"createdAt":{"type":"string","format":"date-time","description":"When the code was entered; sign-ins on this device carry this as their second-factor time."},"lastUsedAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":"string","format":"date-time","description":"createdAt + 30 days; use does not extend it."},"current":{"type":"boolean","description":"The browser asking holds this device's cookie."}},"required":["id","label","createdAt","lastUsedAt","expiresAt","current"]},"RecoveryCodes":{"type":"array","minItems":10,"maxItems":10,"items":{"type":"string","pattern":"^[0-9a-hjkmnp-tv-z]{4}-[0-9a-hjkmnp-tv-z]{4}-[0-9a-hjkmnp-tv-z]{4}$"},"description":"Shown once; only HMACs are stored. Each works once."},"HttpConsentCookieConfig":{"type":"object","required":["available","enabled","host","verified","verificationName","verificationValue","proxyPath","apiOrigin","cookieName","revisionCookieName","validityDays"],"properties":{"available":{"type":"boolean","description":"Included from Light; downgrade disables the bridge without republishing."},"enabled":{"type":"boolean"},"host":{"type":"string"},"verified":{"type":"boolean"},"verificationName":{"type":"string","description":"DNS TXT name for domain ownership verification."},"verificationValue":{"type":"string","description":"DNS TXT value shown only to the authorized organization."},"proxyPath":{"type":"string","const":"/runtime/consent-cookie/"},"apiOrigin":{"type":"string","format":"uri","description":"The API origin to proxy to; keep upstream TLS and Host, forwarding the verified public host as X-Keksonaut-Consent-Host."},"cookieName":{"type":"string","description":"Host-only decision backup cookie name."},"revisionCookieName":{"type":"string","description":"Host-only signed revocation-fence cookie name."},"validityDays":{"type":"integer","minimum":30,"maximum":730,"description":"The website consent validity; regional overrides cannot exceed it."}}},"HttpConsentCookieRuntime":{"type":"object","required":["enabled","origin"],"additionalProperties":false,"properties":{"enabled":{"type":"boolean"},"origin":{"type":"string","format":"uri"},"cleanupOnly":{"type":"boolean","description":"When true, permit DELETE cleanup only. Never restore or copy a choice in this mode."}},"description":"Optional runtime/settings envelope. Omitted when no verified domain exists. A disabled or downgraded verified domain remains available for cleanup only."},"HttpConsentCookieChoice":{"type":"object","additionalProperties":false,"required":["categories","version","ts","exp"],"properties":{"categories":{"type":"object","maxProperties":32,"additionalProperties":{"type":"boolean"}},"services":{"type":"object","maxProperties":200,"additionalProperties":{"type":"boolean"},"default":{}},"version":{"type":"integer","minimum":0},"ts":{"type":"integer","minimum":1},"exp":{"type":"integer","minimum":1},"resetAt":{"type":"string","maxLength":80}},"description":"Millisecond timestamps. Expired choices, unknown category/service IDs and incompatible versions/resets are rejected. The runtime also applies the visitor's current region validity and consent settings."},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{},"issues":{}}}}},"LegalPublishConfirmation":{"type":"object","description":"The customer's confirmation before a legal text goes live (publish, rollback). Without `confirmed: true` the request is refused with 400 confirmation_required.","properties":{"confirmed":{"type":"boolean","enum":[true],"description":"The customer ticked: privacy policy and imprint checked; the generator does not examine the individual case; they are responsible for correctness, completeness and currency."},"confirmationVersion":{"type":"string","description":"Optional. The wording version shown (currently 2026-10-01); the server always records its own current version and answers 400 confirmation_version_mismatch when this differs."}},"required":["confirmed"]},"User":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"name":{"type":"string"},"emailVerified":{"type":"boolean"},"isPlatformAdmin":{"type":"boolean"},"locale":{"type":"string","enum":["de","en","es"]},"twoFactorEnabled":{"type":"boolean","description":"TOTP second factor on. Mandatory for platform admins: /api/admin/* answers 403 two_factor_required without it."}},"required":["id","email","name","emailVerified","isPlatformAdmin","locale","twoFactorEnabled"]},"Organization":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"plan":{"type":"string","enum":["free","light","pro","business250k","business1m","business2m5","ultimate"]},"role":{"type":"string","enum":["owner","admin","editor","billing"]}},"required":["id","name","slug","plan","role"]},"TokenResponse":{"type":"object","properties":{"accessToken":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}},"required":["accessToken","user"]},"SignupResponse":{"allOf":[{"$ref":"#/components/schemas/TokenResponse"},{"type":"object","properties":{"organizationId":{"type":"string"},"dpaAccepted":{"type":"boolean","description":"True when the hosted DPA was incorporated with registration terms or explicitly accepted by a legacy client"},"dpaIncorporated":{"type":"boolean","description":"The current hosted DPA was included with registration terms; no separate signature was manufactured"}},"required":["organizationId","dpaAccepted","dpaIncorporated"]}]},"Member":{"type":"object","properties":{"memberId":{"type":"string"},"userId":{"type":"string"},"email":{"type":"string"},"name":{"type":"string"},"role":{"type":"string","enum":["owner","admin","editor","billing"]},"acceptedAt":{"type":"string","format":"date-time","nullable":true}},"required":["memberId","userId","email","name","role"]},"Invitation":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","admin","editor","billing"]},"expiresAt":{"type":"string","format":"date-time"}},"required":["id","email","role","expiresAt"]},"Website":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"settingsId":{"type":"string"},"installedAt":{"type":"string","format":"date-time","nullable":true},"installSeenAt":{"type":"string","format":"date-time","nullable":true},"archivedAt":{"type":"string","format":"date-time","nullable":true},"consentRetentionDays":{"type":"integer","minimum":30,"maximum":1095},"consentValidityDays":{"type":"integer","minimum":30,"maximum":730},"consentFloatingButton":{"type":"boolean"},"consentResetAt":{"type":"string","format":"date-time","nullable":true},"scanIncludePaths":{"type":"array","items":{"type":"string"},"maxItems":100},"scanExcludePaths":{"type":"array","items":{"type":"string"},"maxItems":100}},"required":["id","name","slug","settingsId"]},"Pagination":{"type":"object","properties":{"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":200},"total":{"type":"integer","minimum":0}},"required":["offset","limit","total"]},"BannerColor":{"type":"string","maxLength":40,"pattern":"^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\\([0-9.,\\s]+\\)|transparent)$","example":"#2457E6"},"BannerStyle":{"type":"object","properties":{"presetId":{"type":["string","null"],"pattern":"^P-\\d{3}$","description":"The gallery preset the design started from (informational)."},"colorScheme":{"type":["string","null"],"enum":["light","dark",null],"description":"Shadow and toggle tones; null derives it from the background."},"fontFamily":{"type":"string","enum":["system","inherit","plus-jakarta-sans","sora","ibm-plex-sans","instrument-serif","space-grotesk","dm-sans","manrope","outfit","jetbrains-mono"],"default":"system","description":"Self-hosted from this API (no third-party font request). system = system UI fonts, inherit = the website's font."},"headingFontFamily":{"type":["string","null"],"enum":["system","inherit","plus-jakarta-sans","sora","ibm-plex-sans","instrument-serif","space-grotesk","dm-sans","manrope","outfit","jetbrains-mono",null],"description":"null: same as fontFamily."},"headingWeight":{"type":"integer","enum":[400,500,600,700,800],"default":700},"radius":{"type":"integer","minimum":0,"maximum":40,"default":16},"buttonRadius":{"type":"integer","minimum":0,"maximum":999,"default":10,"description":"999 renders pill-shaped buttons."},"borderWidth":{"type":"integer","minimum":0,"maximum":4,"default":1},"shadow":{"type":"string","enum":["soft","hard","none"],"default":"soft"},"blur":{"type":"integer","minimum":0,"maximum":40,"default":0,"description":"Backdrop blur in px for translucent backgrounds."}}},"BannerSettingsPatch":{"type":"object","description":"Editable banner settings. Free includes the bottom bar (bar-bottom) with any colors and style, and German/English; the other layouts, an own logo, custom CSS, storage modes and removing banner attribution are available from Light.","properties":{"layout":{"type":"string","enum":["bar-bottom","float-bar","card-left","card-right","card-center","pill","sheet","modal","side","bar-top","modal-center","box-corner"],"description":"The ten design layouts (runtime 0.7.0+). Free includes bar-bottom only. modal-center and box-corner are the layouts of runtime 0.6 and stay valid: without `style` they render as before, with `style` as modal / card-right."},"buttonStyle":{"type":"string","enum":["accept-reject","accept-save","accept-only"]},"colors":{"type":"object","description":"Each color is #rgb, #rgba, #rrggbb, #rrggbbaa, rgb()/rgba() with plain numbers, or `transparent`. Colors without a value are derived from text and accent.","properties":{"background":{"$ref":"#/components/schemas/BannerColor"},"text":{"$ref":"#/components/schemas/BannerColor"},"accent":{"allOf":[{"$ref":"#/components/schemas/BannerColor"}],"description":"Primary button background."},"accentText":{"allOf":[{"$ref":"#/components/schemas/BannerColor"}],"description":"Primary button text."},"muted":{"allOf":[{"$ref":"#/components/schemas/BannerColor"}],"description":"Secondary text (description)."},"border":{"$ref":"#/components/schemas/BannerColor"},"secondaryBackground":{"allOf":[{"$ref":"#/components/schemas/BannerColor"}],"description":"Secondary button background (reject)."},"secondaryText":{"$ref":"#/components/schemas/BannerColor"},"secondaryBorder":{"$ref":"#/components/schemas/BannerColor"},"link":{"allOf":[{"$ref":"#/components/schemas/BannerColor"}],"description":"Text links such as Preferences."}}},"style":{"oneOf":[{"$ref":"#/components/schemas/BannerStyle"},{"type":"null"}],"description":"Design-system styling (runtime 0.7.0+). A partial object is merged into the stored style; null returns to the classic look of runtime 0.6."},"logoUrl":{"type":"string","description":"An HTTP(S) URL or empty string"},"privacyPolicyUrl":{"type":"string"},"imprintUrl":{"type":"string"},"defaultLocale":{"type":"string","maxLength":35},"translations":{"type":"array","maxItems":64,"items":{"type":"object","properties":{"locale":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"acceptAll":{"type":"string"},"rejectAll":{"type":"string"},"save":{"type":"string"},"preferences":{"type":"string"},"policyLinkLabel":{"type":"string"},"privacyPolicyUrl":{"type":"string","description":"This language's privacy policy: an HTTP(S) URL or empty string. Empty or absent: the banner's privacyPolicyUrl applies."},"imprintUrl":{"type":"string","description":"This language's legal notice: an HTTP(S) URL or empty string. Empty or absent: the banner's imprintUrl applies."}},"required":["locale"]}},"hideBranding":{"type":"boolean","description":"Hidden by default from Light; an explicit paid-plan choice to show the badge is retained. Free always shows it."},"autoBlockScripts":{"type":"boolean"},"autoBlockIframes":{"type":"boolean","description":"Missing legacy values follow autoBlockScripts."},"respectGlobalPrivacyControl":{"type":"boolean","default":true},"reloadOnConsent":{"type":"boolean","default":false},"showOverlay":{"type":"boolean","default":false},"revisitButton":{"type":"object","description":"A missing enabled flag follows the website consentFloatingButton setting.","properties":{"enabled":{"type":"boolean"},"position":{"type":"string","enum":["bottom-left","bottom-right","top-left","top-right"],"default":"bottom-left"},"style":{"type":"string","enum":["icon","pill","tab"],"default":"icon"},"icon":{"type":"string","enum":["mascot","cookie","shield","settings","fingerprint","lock","sliders"],"default":"mascot"},"text":{"type":"string","maxLength":100,"default":"","description":"Empty uses the localized preferences label."},"backgroundColor":{"type":"string","default":"","description":"Empty uses the banner accent color."},"iconColor":{"type":"string","default":"","description":"Empty uses a contrasting color."},"offsetX":{"type":"integer","minimum":0,"maximum":200,"default":20},"offsetY":{"type":"integer","minimum":0,"maximum":200,"default":20}}},"disabledPages":{"type":"array","maxItems":100,"default":[],"items":{"type":"string","maxLength":300},"description":"Path or HTTP(S) URL patterns. * matches any characters. Matching ignores fragments and includes query parameters only when the pattern contains a literal ?. No raw regular expressions or fragments."},"buttons":{"type":"object","properties":{"acceptAll":{"type":"boolean","default":true},"rejectAll":{"type":"boolean","default":true},"preferences":{"type":"boolean","default":true}}},"googleConsentMode":{"type":"object","properties":{"enabled":{"type":"boolean","default":true},"urlPassthrough":{"type":"boolean","default":false}}},"googleTagManager":{"type":"object","properties":{"enabled":{"type":"boolean","default":false},"containerId":{"type":"string","maxLength":100,"pattern":"^(GTM-[A-Z0-9]+)?$","default":""}}},"integrations":{"type":"object","description":"Missing switches retain service-based automatic integration. No Matomo tracking IDs or auto-loader are configured here.","properties":{"microsoftUet":{"type":"boolean"},"microsoftClarity":{"type":"boolean"},"matomo":{"type":"boolean"},"amazonConsent":{"type":"boolean"}}},"regionRules":{"type":"array","maxItems":100,"default":[],"description":"Country rules take precedence over named regions, then world. The first enabled match within the same priority applies. A country requires trusted server-side detection. Unknown visitors use protected opt-in defaults. Europe and worldwide optional-service rules require explicit opt-in. Regional buttons cannot re-enable globally disabled actions.","items":{"type":"object","required":["id","region"],"properties":{"id":{"type":"string","minLength":1,"maxLength":100,"pattern":"^[a-zA-Z0-9_-]+$"},"name":{"type":"string","maxLength":100},"enabled":{"type":"boolean","default":true},"region":{"type":"string","enum":["world","eu","us","br","ca","apac","mea","countries"]},"countries":{"type":"array","maxItems":249,"uniqueItems":true,"items":{"type":"string","pattern":"^[A-Z]{2}$"},"description":"ISO 3166-1 alpha-2 codes; required for countries rules."},"model":{"type":"string","enum":["opt-in","opt-out","notice-only","hidden"],"default":"opt-in"},"buttons":{"type":"object","properties":{"acceptAll":{"type":"boolean","default":true},"rejectAll":{"type":"boolean","default":true},"preferences":{"type":"boolean","default":true}}},"dismissOnScroll":{"type":"boolean","default":false},"dismissAfterSeconds":{"type":"integer","minimum":0,"maximum":3600,"default":0},"consentDurationDays":{"type":"integer","minimum":30,"maximum":730,"description":"Missing follows the website's consent validity. An explicit regional duration must not exceed website consentValidityDays; raise website validity and retention first."},"reconsentOnConfigChange":{"type":"boolean","default":true},"cookieWall":{"type":"boolean","default":false},"doNotSell":{"type":"boolean","default":false}}}},"consentStorage":{"type":"string","enum":["localStorage","sessionStorage","cookie"]},"customCss":{"type":"string","maxLength":20000,"description":"Flat CSS rules scoped to consent widgets; external resources and CSS escapes are rejected."},"buttonOrder":{"type":"array","minItems":3,"maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["preferences","reject","accept"]}},"respectDoNotTrack":{"type":"boolean"}}},"PublicWebsite":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"settingsId":{"type":"string"}},"required":["id","name","slug","settingsId"]},"AnalyticsSummary":{"type":"object","description":"Consents per type and the page views over the range. Since runtime 0.4.0 there are no banner impressions (the former `impression` field is removed): nothing is reported before a visitor decides. Since runtime 0.5.0 only consents are reported — a refusal stays in the visitor's browser — so the former `reject_all`, `decisions`, `acceptRate` and `rejectRate` fields are removed. A consent is a decision granting at least one optional category or service; a banner with required categories only offers none (`optionalChoices` false), so it has no consents to count rather than zero consents.","properties":{"breakdown":{"type":"object","nullable":true,"description":"Business and higher: consent counts classified from already recorded User-Agent headers, never raw user agents. Free/Light return null.","properties":{"browsers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"count":{"type":"integer","minimum":0}},"required":["name","count"]}},"operatingSystems":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"count":{"type":"integer","minimum":0}},"required":["name","count"]}}},"required":["browsers","operatingSystems"]},"pageviews":{"type":"integer","description":"Page views — the banner settings fetched on a page load — of the Europe/Berlin calendar days the range touches."},"accept_all":{"type":"integer"},"save":{"type":"integer"},"update":{"type":"integer"},"consents":{"type":"integer","description":"Every stored consent: accept_all + save + update"},"optionalChoices":{"type":"boolean","description":"Whether the banner live now offers optional choices — at least one category it does not mark required. False: no decision on it is a consent, none is stored, there is nothing to count."},"pageviewsWithOptionalChoices":{"type":"integer","description":"The page views of the range's days on which the banner offered optional choices (or a consent is stored). Equals `pageviews` when it offered them on every day."},"consentsPer1000Pageviews":{"type":"number","description":"consents × 1000 / pageviewsWithOptionalChoices, one decimal; 0 without such page views. Page views of days on which the banner offered nothing to consent to are left out. Page views include repeat views, so this is no consent rate."}},"required":["pageviews","breakdown","accept_all","save","update","consents","optionalChoices","pageviewsWithOptionalChoices","consentsPer1000Pageviews"]},"AnalyticsTimeseriesPoint":{"type":"object","description":"One Europe/Berlin calendar day with page views or consents (days without either are left out).","properties":{"date":{"type":"string","format":"date"},"pageviews":{"type":"integer"},"accept_all":{"type":"integer"},"save":{"type":"integer"},"update":{"type":"integer"},"optionalChoices":{"type":"boolean","description":"Whether a banner live that day offered optional choices, or a consent is stored that day. False: nothing could be consented to, so the day's zero consents are nothing to count."}},"required":["date","pageviews","accept_all","save","update","optionalChoices"]},"ConsentReport":{"type":"object","description":"A visitor's consent as runtime.js (>= 0.5.0) reports it: a decision granting at least one optional category or offered service. No visitor id: an `anonId` sent by an older cached runtime is accepted and discarded, as are `host`, `path` and `userAgent` (runtime <= 0.2.0). A refusal of runtime <= 0.4.0 (`reject_all`, or a choice granting nothing optional) is answered 204 and not stored.","properties":{"settingsId":{"type":"string"},"type":{"type":"string","enum":["accept_all","save","update"]},"version":{"type":"integer","minimum":0,"description":"Banner version the decision was made for"},"categories":{"type":"object","additionalProperties":{"type":"boolean"}},"services":{"type":"object","additionalProperties":{"type":"boolean"}}},"required":["settingsId","type"]},"ConsentCreatedWebhook":{"type":"object","description":"Body of the consent.created webhook (`cm-event: consent.created`, signed in `cm-signature`). Consents only — a refusal is not reported (runtime 0.5.0), so `type` is never `reject_all`. The `anonId` field was removed with runtime 0.4.0 — consent reports carry no visitor id.","properties":{"event":{"type":"string","enum":["consent.created"]},"data":{"type":"object","properties":{"deliveryId":{"type":"string"},"websiteId":{"type":"string"},"eventId":{"type":"string"},"type":{"type":"string","enum":["accept_all","save","update"]},"categories":{"type":"object","additionalProperties":{"type":"boolean"}},"services":{"type":"object","additionalProperties":{"type":"boolean"}},"occurredAt":{"type":"string","format":"date-time"}},"required":["deliveryId","websiteId","eventId","type","categories","services","occurredAt"]}},"required":["event","data"]}},"parameters":{"OrgHeader":{"name":"x-org-id","in":"header","required":true,"schema":{"type":"string"}},"OrgIdPath":{"name":"id","in":"path","required":true,"schema":{"type":"string"}},"FromQuery":{"name":"from","in":"query","required":false,"description":"Start of the time range (inclusive). ISO 8601 date or datetime.","schema":{"type":"string","example":"2026-01-01"}},"ToQuery":{"name":"to","in":"query","required":false,"description":"End of the time range (inclusive). ISO 8601 date or datetime.","schema":{"type":"string","example":"2026-01-31T23:59:59Z"}},"OffsetQuery":{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0}},"LimitQuery":{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}},"responses":{"Error":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"paths":{"/api/dpa/document":{"get":{"summary":"Read the current full DPA and annexes","responses":{"200":{"description":"Current contract; no-store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicDpaDocument"}}}}}}},"/api/dpa":{"get":{"summary":"Read organization DPA status and latest acceptance","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"Status; no-store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DpaStatus"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/dpa/acceptances":{"get":{"summary":"Read all organization DPA acceptance snapshots","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"History, newest first; no-store","content":{"application/json":{"schema":{"type":"object","properties":{"acceptances":{"type":"array","items":{"$ref":"#/components/schemas/DpaAcceptance"}},"incorporations":{"type":"array","items":{"$ref":"#/components/schemas/DpaIncorporation"}}},"required":["acceptances","incorporations"]}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"}}}},"/api/dpa/accept":{"post":{"summary":"Explicitly conclude the organization DPA (owner/admin user only)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgHeader"}],"description":"API keys cannot conclude contracts. Version and documentHash must match the ready current offer. Retries retain the original time, actor and controller; acceptedAt is server-generated.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"version":{"type":"string"},"documentHash":{"type":"string","pattern":"^[a-f0-9]{64}$"},"accepted":{"type":"boolean","const":true},"authorized":{"type":"boolean","const":true},"signatoryName":{"type":"string"},"controller":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string"},"address":{"type":"string"},"email":{"type":"string","format":"email"}},"required":["name","address","email"]}},"required":["version","documentHash","accepted","authorized","signatoryName","controller"]}}}},"responses":{"200":{"description":"Current status and immutable acceptance snapshot","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DpaStatus"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"409":{"description":"dpa_version_mismatch or dpa_not_applicable"},"503":{"description":"dpa_not_configured: operator facts have not been verified"}}}},"/api/websites/{id}/banner/http-cookie":{"get":{"summary":"Read HTTP-consent-cookie setup","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"HTTP-consent-cookie setup","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpConsentCookieConfig"}}}}}},"put":{"summary":"Configure an HTTP-consent-cookie subdomain","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"description":"Owner/admin/editor; Light or higher. Use a distinct subdomain on the website's registrable domain. A changed host generates a new proof and clears verification. Enable only after DNS and HTTPS verification succeed and the customer has updated the privacy notice. Changing the host or disabling invalidates existing backups through a server-side domain revision.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["host"],"properties":{"host":{"type":"string","maxLength":253},"enabled":{"type":"boolean"},"privacyNoticeAcknowledged":{"type":"boolean","description":"Must be true when enabling a previously disabled setup; confirms that the additional cookie storage is described in the privacy notice."}}}}}},"responses":{"200":{"description":"HTTP-consent-cookie setup","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpConsentCookieConfig"}}}},"400":{"description":"Invalid domain, setup not verified, or consent_cookie_privacy_notice_required"},"403":{"description":"Paid plan or write role required"},"409":{"description":"Domain already used by another website"}}}},"/api/websites/{id}/banner/http-cookie/verify":{"post":{"summary":"Verify DNS ownership and HTTPS proxy","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"description":"Owner/admin/editor; at most five checks per minute. Requires the exact generated TXT record and a signed HTTPS probe response. The network request is SSRF-protected and follows no redirects.","responses":{"200":{"description":"HTTP-consent-cookie setup","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpConsentCookieConfig"}}}},"400":{"description":"TXT, HTTPS or proxy verification failed"},"429":{"description":"Too many verification attempts"}}}},"/runtime/consent-cookie/{settingsId}":{"get":{"summary":"Restore a private HTTP choice backup","description":"Call through the website's verified HTTPS consent subdomain with fetch credentials: include. Requires a verified setup and an exact registered same-site HTTPS Origin. Grant and restore additionally require an enabled paid-plan setup; cleanup remains available after disabling or downgrading. Cookies are signed, host-only, Secure, HttpOnly, SameSite=Strict. This is a private choice backup, not consent evidence. No visitor database record or consent event is created.","security":[],"parameters":[{"name":"settingsId","in":"path","required":true,"schema":{"type":"string"}},{"name":"Origin","in":"header","required":true,"schema":{"type":"string","format":"uri"},"description":"Exact HTTPS origin of a registered production domain on the same site as the verified consent subdomain."}],"responses":{"200":{"description":"Valid backed-up choice or null; absent/invalid revocation markers initialize a new marker and return null, and invalid decision cookies are cleared","content":{"application/json":{"schema":{"type":"object","properties":{"consent":{"anyOf":[{"$ref":"#/components/schemas/HttpConsentCookieChoice"},{"type":"null"}]}}}}}},"403":{"description":"Origin not allowed"}}},"post":{"summary":"Back up the visitor's explicit choice in an HTTP cookie","description":"Call through the website's verified HTTPS consent subdomain with fetch credentials: include. Requires a verified setup and an exact registered same-site HTTPS Origin. Grant and restore additionally require an enabled paid-plan setup; cleanup remains available after disabling or downgrading. Cookies are signed, host-only, Secure, HttpOnly, SameSite=Strict. This is a private choice backup, not consent evidence. No visitor database record or consent event is created.","security":[],"parameters":[{"name":"settingsId","in":"path","required":true,"schema":{"type":"string"}},{"name":"Origin","in":"header","required":true,"schema":{"type":"string","format":"uri"},"description":"Exact HTTPS origin of a registered production domain on the same site as the verified consent subdomain."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["consent"],"properties":{"consent":{"$ref":"#/components/schemas/HttpConsentCookieChoice"}}}}}},"responses":{"204":{"description":"Signed cookie set"},"400":{"description":"Choice invalid, expired or too large for a cookie"},"403":{"description":"Origin not allowed"},"409":{"description":"consent_cookie_revision_required: missing or invalid marker; no Set-Cookie. Initialize with GET, then retry only the still-current explicit choice."},"415":{"description":"application/json required"}}},"delete":{"summary":"Clear the visitor's private HTTP choice backup","description":"Call through the website's verified HTTPS consent subdomain with fetch credentials: include. Requires a verified setup and an exact registered same-site HTTPS Origin. Grant and restore additionally require an enabled paid-plan setup; cleanup remains available after disabling or downgrading. Cookies are signed, host-only, Secure, HttpOnly, SameSite=Strict. This is a private choice backup, not consent evidence. No visitor database record or consent event is created. DELETE rotates the signed revocation marker and clears the backup. An optional validated fully optional refusal can be copied atomically under the new marker. A partial withdrawal with remaining optional grants only clears the backup; it remains local. Missing or invalid bodies still perform cleanup.","security":[],"parameters":[{"name":"settingsId","in":"path","required":true,"schema":{"type":"string"}},{"name":"Origin","in":"header","required":true,"schema":{"type":"string","format":"uri"},"description":"Exact HTTPS origin of a registered production domain on the same site as the verified consent subdomain."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"consent":{"$ref":"#/components/schemas/HttpConsentCookieChoice"}}}}}},"responses":{"204":{"description":"Backup cleared; revocation marker rotated, with a valid full refusal backed up only while enabled on a paid plan"},"403":{"description":"Origin not allowed"}}}},"/api/websites/{id}/consent/export.csv":{"get":{"summary":"Download the complete stored consent evidence as streaming CSV","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}}],"description":"Includes occurredAt,type,version,host,path,categories,services,originVerified,eventId,bannerVersionId,ipHash,userAgent. The compatibility path column is always empty. Formula-prefixed values are escaped. This is a website consent log, not an entire organization/configuration portability export.","responses":{"200":{"description":"CSV attachment; no-store","content":{"text/csv":{"schema":{"type":"string"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/websites/{id}/consent/export.json":{"get":{"summary":"Download the complete stored consent evidence as streaming JSON","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"},{"name":"from","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","schema":{"type":"string","format":"date-time"}}],"description":"Every stored proof field, without legacy paths or visitor identifiers. This is a website consent log, not an entire organization/configuration portability export.","responses":{"200":{"description":"JSON array attachment; no-store","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ConsentEvidence"}}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/auth/2fa":{"get":{"summary":"Second-factor status of the current user","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Status","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TwoFactorStatus"}}}},"401":{"$ref":"#/components/responses/Error"}}}},"/api/auth/2fa/setup":{"post":{"summary":"Start the enrolment: a new TOTP secret (SHA-1, 6 digits, 30 s)","description":"The secret stays pending until POST /enable confirms a code from it (30 minutes). Calling again replaces it. The webapp renders `otpauthUri` as a QR code locally.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Secret for the authenticator app","content":{"application/json":{"schema":{"type":"object","properties":{"secret":{"type":"string","description":"Base32, for manual entry"},"otpauthUri":{"type":"string","description":"otpauth://totp/… — the QR code's content"},"issuer":{"type":"string"},"account":{"type":"string"},"algorithm":{"type":"string","enum":["SHA1"]},"digits":{"type":"integer","enum":[6]},"period":{"type":"integer","enum":[30]}},"required":["secret","otpauthUri","issuer","account","algorithm","digits","period"]}}}},"409":{"description":"two_factor_already_enabled"}}}},"/api/auth/2fa/enable":{"post":{"summary":"Confirm the enrolment with the first app code","description":"Switches 2FA on, answers the ten recovery codes (the only time they are shown) and an access token for this session, marked as second-factor verified. Audited (user.2fa.enabled); the user is mailed.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","pattern":"^\\d{6}$"}},"required":["code"]}}}},"responses":{"200":{"description":"Enabled","content":{"application/json":{"schema":{"type":"object","properties":{"recoveryCodes":{"$ref":"#/components/schemas/RecoveryCodes"},"accessToken":{"type":"string"},"status":{"$ref":"#/components/schemas/TwoFactorStatus"}},"required":["recoveryCodes","accessToken","status"]}}}},"400":{"description":"two_factor_setup_expired — start again with POST /setup"},"401":{"description":"invalid_two_factor_code — wrong, expired or already used code"},"409":{"description":"two_factor_already_enabled, two_factor_setup_changed"},"429":{"description":"too_many_attempts — per-account backoff (five attempts, then 30 s doubling up to 1 h between attempts). Retry-After and details.retryAfterSeconds say how long."}}}},"/api/auth/2fa/verify":{"post":{"summary":"Second sign-in step: exchange the challenge and a code for the session","description":"The challenge comes from the login answer (`challengeToken`) or, after a Google/Microsoft sign-in, from the HttpOnly cookie cm_2fa_challenge (send no challengeToken then). Only now is the refresh cookie set. Same Origin rule as /api/auth/refresh. A recovery code is audited (user.2fa.recovery_code_used) and mailed. With `trustDevice: true` the browser also gets cm_trusted_device (HttpOnly, path /api/auth, 30 days, not extended by use): the next password or Google/Microsoft sign-in of this account in it skips the code (audited user.2fa.trusted_device.created / .used).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"challengeToken":{"type":"string"},"code":{"type":"string","minLength":6,"maxLength":32,"description":"A 6-digit code from the authenticator app, or one of the recovery codes (xxxx-xxxx-xxxx; case, spaces and dashes are ignored)."},"trustDevice":{"type":"boolean","default":false,"description":"\"Trust this device for 30 days\""}},"required":["code"]}}}},"responses":{"200":{"description":"Signed in","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/TokenResponse"},{"type":"object","properties":{"next":{"type":"string","description":"OAuth only: the same-site path the sign-in was headed to"}}}]}}}},"401":{"description":"invalid_two_factor_code, or two_factor_challenge_invalid (expired, forged, or the account changed since the first step — sign in again)"},"403":{"description":"origin_not_allowed"},"429":{"description":"too_many_attempts — per-account backoff (five attempts, then 30 s doubling up to 1 h between attempts). Retry-After and details.retryAfterSeconds say how long."}}}},"/api/auth/2fa/step-up":{"post":{"summary":"Confirm the current session with a code (step-up)","description":"Answers an access token whose second-factor time is now, and marks the session's refresh token likewise. Catalog-wide admin writes need one from the last 10 minutes (403 step_up_required otherwise); /api/admin/* needs a session verified since the current enrolment.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":6,"maxLength":32,"description":"A 6-digit code from the authenticator app, or one of the recovery codes (xxxx-xxxx-xxxx; case, spaces and dashes are ignored)."}},"required":["code"]}}}},"responses":{"200":{"description":"Verified","content":{"application/json":{"schema":{"type":"object","properties":{"accessToken":{"type":"string"},"verifiedAt":{"type":"string","format":"date-time"},"maxAgeSeconds":{"type":"integer","enum":[600]}},"required":["accessToken","verifiedAt","maxAgeSeconds"]}}}},"401":{"description":"invalid_two_factor_code — wrong, expired or already used code"},"409":{"description":"two_factor_not_enabled"},"429":{"description":"too_many_attempts — per-account backoff (five attempts, then 30 s doubling up to 1 h between attempts). Retry-After and details.retryAfterSeconds say how long."}}}},"/api/auth/2fa/recovery-codes":{"post":{"summary":"Replace all recovery codes","description":"Needs a valid code. The old codes stop working, and so does every trusted device. Audited (user.2fa.recovery_codes_regenerated).","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":6,"maxLength":32,"description":"A 6-digit code from the authenticator app, or one of the recovery codes (xxxx-xxxx-xxxx; case, spaces and dashes are ignored)."}},"required":["code"]}}}},"responses":{"200":{"description":"New codes","content":{"application/json":{"schema":{"type":"object","properties":{"recoveryCodes":{"$ref":"#/components/schemas/RecoveryCodes"},"status":{"$ref":"#/components/schemas/TwoFactorStatus"}},"required":["recoveryCodes","status"]}}}},"401":{"description":"invalid_two_factor_code — wrong, expired or already used code"},"409":{"description":"two_factor_not_enabled"},"429":{"description":"too_many_attempts — per-account backoff (five attempts, then 30 s doubling up to 1 h between attempts). Retry-After and details.retryAfterSeconds say how long."}}}},"/api/auth/2fa/trusted-devices":{"get":{"summary":"Browsers trusted for 30 days (newest first, at most 10)","description":"Only devices that still count: not expired, created since the current enrolment and recovery codes, and in the current session generation. `current` marks the browser asking (its cm_trusted_device cookie).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Devices","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/TrustedDevice"}}},"required":["devices"]}}}},"401":{"$ref":"#/components/responses/Error"}}},"delete":{"summary":"Stop trusting every browser","description":"Removes all of the user's trusted devices and clears this browser's cookie. No step-up. Audited (user.2fa.trusted_device.revoked, reason user_all).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"None left","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","maxItems":0,"items":{"$ref":"#/components/schemas/TrustedDevice"}}},"required":["devices"]}}}},"401":{"$ref":"#/components/responses/Error"}}}},"/api/auth/2fa/trusted-devices/{id}":{"delete":{"summary":"Stop trusting one browser","description":"Clears this browser's cookie when it is the one removed. No step-up. Audited (user.2fa.trusted_device.revoked, reason user). Answers the remaining list.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Remaining devices","content":{"application/json":{"schema":{"type":"object","properties":{"devices":{"type":"array","items":{"$ref":"#/components/schemas/TrustedDevice"}}},"required":["devices"]}}}},"401":{"$ref":"#/components/responses/Error"},"404":{"description":"not_found — no such device of this user"}}}},"/api/auth/2fa/disable":{"post":{"summary":"Switch the second factor off","description":"Needs the password (accounts that have one; per-address backoff as at login) or, for Google/Microsoft-only accounts, a sign-in within the last 10 minutes — plus a valid code. Audited (user.2fa.disabled); the user is mailed. Every trusted device goes too. A platform admin loses admin API access until enrolling again.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string"},"code":{"type":"string","minLength":6,"maxLength":32,"description":"A 6-digit code from the authenticator app, or one of the recovery codes (xxxx-xxxx-xxxx; case, spaces and dashes are ignored)."}},"required":["code"]}}}},"responses":{"200":{"description":"Off","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/TwoFactorStatus"}},"required":["status"]}}}},"400":{"description":"password_required"},"401":{"description":"invalid_password, invalid_two_factor_code"},"403":{"description":"reauth_required — sign in with Google/Microsoft again, then retry within 10 minutes"},"409":{"description":"two_factor_not_enabled"},"429":{"description":"too_many_attempts — per-account backoff (five attempts, then 30 s doubling up to 1 h between attempts). Retry-After and details.retryAfterSeconds say how long."}}}},"/health":{"get":{"summary":"Health check","responses":{"200":{"description":"Service is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string"},"service":{"type":"string"},"uptime":{"type":"number"},"mongo":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}}}}}}}},"/api/auth/signup":{"post":{"summary":"Register a new user and create an organization","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":10},"name":{"type":"string"},"organizationName":{"type":"string"},"acceptedTerms":{"type":"boolean","enum":[true],"description":"The user accepted the Terms of Service, incorporated DPA and Privacy Policy and confirmed business use."},"legalVersion":{"type":"string","description":"Optional. The Terms/Privacy version shown to the user; the server always records its own current version and answers 400 legal_version_mismatch when this differs."},"dpa":{"$ref":"#/components/schemas/DpaAcceptanceInput","description":"Legacy explicit acceptance input. Current registration incorporates the hosted DPA through the combined terms checkbox without separate signature or controller fields."},"dpaDocument":{"$ref":"#/components/schemas/DpaDocumentReference","description":"Optional identity of the complete DPA shown with the combined registration terms; stale snapshots are rejected."},"token":{"type":"string","description":"Fill-time token from GET /api/auth/signup-token. 400 signup_token_invalid (missing/forged), signup_too_fast (< 3 s old), signup_token_expired (> 24 h)."},"website":{"type":"string","description":"Honeypot: render hidden and send empty. Filled → 201 {ok:true}, nothing created."},"plan":{"type":"string","description":"Optional attribution: the plan picked on the pricing page. Recorded on the signup event; a malformed value is dropped, never refused."},"interval":{"type":"string","enum":["monthly","yearly"],"description":"Optional, with a paid `plan`: the billing period picked on the pricing page. Both then ride on the verification link (`&plan=…&interval=…`) so the webapp can offer that plan for booking after verification. Nothing is booked or granted."}},"required":["email","password","acceptedTerms","token"]}}}},"responses":{"201":{"description":"User created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupResponse"}}}},"400":{"$ref":"#/components/responses/Error"},"409":{"$ref":"#/components/responses/Error"},"429":{"description":"rate_limited — 5 signups/hour and 20/day per client address (IPv6 /64), 30/day per IPv4 /24"},"503":{"description":"dpa_not_configured on hosted registration when verified operator facts are missing, or signup_cleanup_failed"}}}},"/api/auth/oauth/{provider}/start":{"post":{"summary":"Start an explicit OAuth signup without sending controller data to the provider","description":"Requires Origin to equal APP_URL. The combined registration terms incorporate the DPA. Its public version/hash travel in signed nonce-bound provider state and are rechecked at callback. Legacy explicit controller details remain confined to an HttpOnly cookie. Ordinary existing-account logins create no acceptance or incorporation evidence.","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["google","microsoft"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"acceptedTerms":{"type":"boolean"},"legalVersion":{"type":"string"},"returnTo":{"type":"string"},"locale":{"type":"string","enum":["de","en","es"]},"dpa":{"$ref":"#/components/schemas/DpaAcceptanceInput"},"dpaDocument":{"$ref":"#/components/schemas/DpaDocumentReference"}}}}}},"responses":{"200":{"description":"Provider authorization URL; private AVV details stay in a signed browser cookie","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}},"required":["url"]}}}},"400":{"description":"Invalid acceptance, legal_version_mismatch or dpa_flow_too_large"},"403":{"description":"invalid_origin"},"409":{"description":"dpa_version_mismatch or dpa_not_applicable"},"503":{"description":"dpa_not_configured"}}}},"/api/auth/signup-token":{"get":{"summary":"Fill-time token for the signup form","responses":{"200":{"description":"{ token, minAgeSeconds: 3, maxAgeSeconds: 86400 } (Cache-Control: no-store)"}}}},"/api/auth/login":{"post":{"summary":"Log in with email and password","description":"Accounts with the second factor on get a TwoFactorChallenge instead of a session (no refresh cookie); POST /api/auth/2fa/verify completes the sign-in. A valid cm_trusted_device cookie of the same account (\"trust this device for 30 days\") stands in for the code once the password is right; the session's second-factor time is then when that code was entered, so step-up still asks for a fresh one. Any other trusted-device cookie is cleared. Per-address backoff (audit M4): five attempts, then 2 s doubling up to 15 min between attempts, for known and unknown addresses alike, on top of the per-client limit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}},"required":["email","password"]}}}},"responses":{"200":{"description":"Logged in, or the second step is due","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/TokenResponse"},{"$ref":"#/components/schemas/TwoFactorChallenge"}]}}}},"401":{"$ref":"#/components/responses/Error"},"429":{"description":"too_many_attempts (per address, Retry-After) or the per-client limit"}}}},"/api/auth/refresh":{"post":{"summary":"Rotate refresh token (reads cm_refresh cookie)","description":"A request with an Origin header outside CORS_ORIGINS/APP_URL is refused before the cookie is read. Requests without Origin are accepted.","responses":{"200":{"description":"New access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"401":{"$ref":"#/components/responses/Error"},"403":{"description":"origin_not_allowed — the Origin header is not an allowed application origin"}}}},"/api/auth/logout":{"post":{"summary":"Revoke refresh token and clear cookie","description":"Same Origin rule as /api/auth/refresh.","responses":{"204":{"description":"Logged out"},"403":{"description":"origin_not_allowed — the Origin header is not an allowed application origin"}}}},"/api/auth/forgot-password":{"post":{"summary":"Initiate password reset","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"}},"required":["email"]}}}},"responses":{"202":{"description":"Accepted"}}}},"/api/auth/reset-password":{"post":{"summary":"Complete password reset","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"password":{"type":"string","minLength":10}},"required":["token","password"]}}}},"responses":{"200":{"description":"Password updated"},"400":{"$ref":"#/components/responses/Error"}}}},"/api/auth/verify-email":{"post":{"summary":"Verify email address","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]}}}},"responses":{"200":{"description":"Email verified"},"400":{"$ref":"#/components/responses/Error"}}}},"/api/auth/me":{"get":{"summary":"Get current user and memberships","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current user","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"},"organizations":{"type":"array","items":{"$ref":"#/components/schemas/Organization"}}},"required":["user","organizations"]}}}},"401":{"$ref":"#/components/responses/Error"}}}},"/api/organizations":{"get":{"summary":"List organizations the current user belongs to","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of organizations","content":{"application/json":{"schema":{"type":"object","properties":{"organizations":{"type":"array","items":{"$ref":"#/components/schemas/Organization"}}},"required":["organizations"]}}}}}},"post":{"summary":"Create a new organization","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}}}},"responses":{"201":{"description":"Organization created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}}}}},"/api/organizations/accept-invite":{"post":{"summary":"Accept an organization invitation","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"]}}}},"responses":{"200":{"description":"Invitation accepted","content":{"application/json":{"schema":{"type":"object","properties":{"orgId":{"type":"string"},"role":{"type":"string","enum":["owner","admin","editor","billing"]}},"required":["orgId","role"]}}}}}}},"/api/organizations/{id}/members":{"get":{"summary":"List organization members","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgIdPath"},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"Members list","content":{"application/json":{"schema":{"type":"object","properties":{"members":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}},"required":["members"]}}}}}}},"/api/organizations/{id}/invite":{"post":{"summary":"Invite a user to the organization (owner/admin only)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgIdPath"},{"$ref":"#/components/parameters/OrgHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","admin","editor","billing"]}},"required":["email"]}}}},"responses":{"201":{"description":"Invitation created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invitation"}}}},"403":{"description":"email_not_verified — the inviting user has not confirmed their e-mail address (also insufficient_role, owner_required)"},"429":{"description":"invite_rate_limited — 20 invitations per organization and 20 per inviting user within 24 hours; details: { scope: organization|user, limit, retryAfterSeconds }, plus a Retry-After header"}}}},"/api/organizations/{id}/members/{memberId}":{"patch":{"summary":"Update a member's role (owner/admin only)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgIdPath"},{"name":"memberId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["owner","admin","editor","billing"]}},"required":["role"]}}}},"responses":{"204":{"description":"Updated"}}},"delete":{"summary":"Remove a member (owner/admin only)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgIdPath"},{"name":"memberId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"204":{"description":"Removed"}}}},"/api/websites":{"get":{"summary":"List websites in current org","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"websites":{"type":"array","items":{"$ref":"#/components/schemas/Website"}}}}}}}}},"post":{"summary":"Create a website","description":"Hosted service: requires the current organization DPA before activation, including API calls. dpa_required (409) or dpa_not_configured (503) otherwise.","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string"},"domain":{"type":"string"},"bannerLocale":{"type":"string","description":"The banner's first language (e.g. de). Default: en. Plans with German and English only start other languages in English."}},"required":["name"]}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Website"}}}}}}},"/api/websites/{id}":{"get":{"summary":"Get website","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Website"}}}}}},"patch":{"summary":"Update website, consent lifecycle and crawler path filters","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"consentRetentionDays":{"type":"integer","minimum":30,"maximum":1095},"consentValidityDays":{"type":"integer","minimum":30,"maximum":730},"consentFloatingButton":{"type":"boolean"},"resetConsent":{"type":"boolean","description":"True invalidates decisions made before the server's current time; historical evidence is retained."},"scanIncludePaths":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":500,"pattern":"^/"}},"scanExcludePaths":{"type":"array","maxItems":100,"items":{"type":"string","maxLength":500,"pattern":"^/"}}}}}}},"responses":{"200":{"description":"Updated"}}},"delete":{"summary":"Archive website","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"204":{"description":"Archived"}}}},"/api/websites/{id}/domains":{"get":{"summary":"List domains","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Add domain","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":"string"},"isPrimary":{"type":"boolean"},"environment":{"type":"string","enum":["production","staging","development"]}},"required":["host"]}}}},"responses":{"201":{"description":"Created"}}}},"/api/websites/{id}/domains/{domainId}":{"patch":{"summary":"Update domain","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"domainId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"Updated"}}},"delete":{"summary":"Remove domain","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"domainId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"204":{"description":"Removed"}}}},"/api/websites/{id}/banner":{"get":{"summary":"Get banner config + categories","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"OK"}}},"put":{"summary":"Update banner config","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BannerSettingsPatch"}}}},"security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"Updated"}}}},"/api/websites/{id}/banner/publish":{"post":{"summary":"Publish banner (creates immutable version)","description":"Hosted service requires current DPA acceptance: 409 dpa_required or 503 dpa_not_configured. Unsafe banner settings return 400 invalid_banner_behavior with details.issues containing field paths, codes and messages.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"201":{"description":"Published"}}}},"/api/websites/{id}/banner/versions":{"get":{"summary":"List published versions","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"OK"}}}},"/api/websites/{id}/banner/versions/{versionId}":{"get":{"summary":"Read a published banner version for preview","description":"Read-only, organization- and website-bound historical snapshot. Stored visibility is preserved; current draft settings and plan defaults are not applied.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"versionId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"Version metadata and its stored banner, categories and services; internal branding preference metadata is omitted.","content":{"application/json":{"schema":{"type":"object","required":["id","version","publishedAt","publishedBy","snapshot"],"properties":{"id":{"type":"string"},"version":{"type":"integer"},"publishedAt":{"type":"string","format":"date-time"},"publishedBy":{"type":"string"},"snapshot":{"type":"object","required":["banner","categories","services"],"properties":{"banner":{"type":"object","additionalProperties":true},"categories":{"type":"array","items":{"type":"object","additionalProperties":true}},"services":{"type":"array","items":{"type":"object","additionalProperties":true}}}}}}}}},"401":{"description":"Authentication required"},"403":{"description":"Not a member of this organization"},"404":{"description":"Website or version not found in this organization"}}}},"/api/websites/{id}/banner/rollback/{versionId}":{"post":{"summary":"Rollback to a previous version (owner/admin)","description":"Hosted service requires current DPA acceptance before any mutation: 409 dpa_required or 503 dpa_not_configured.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"versionId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"201":{"description":"Rolled back"}}}},"/api/websites/{id}/categories/{categoryId}":{"patch":{"summary":"Update a consent category","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"categoryId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"Updated"}}}},"/api/websites/{id}/services":{"get":{"summary":"List services","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Create a service","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"201":{"description":"Created"}}}},"/api/websites/{id}/services/from-template":{"post":{"summary":"Create a service from a template","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"templateSlug":{"type":"string"},"overrides":{"type":"object","properties":{"name":{"type":"string"},"category":{"type":"string"}}}},"required":["templateSlug"]}}}},"responses":{"201":{"description":"Created"}}}},"/api/websites/{id}/services/{serviceId}":{"patch":{"summary":"Update a service","description":"Fields not sent stay as stored. `i18n` holds the description (and cookie texts) per banner language (de, en, es, fr, it, nl, pt, pl) and merges per language and field; an empty description means the base `description` in that language.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"serviceId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"Updated"}}},"delete":{"summary":"Remove a service","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"serviceId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"204":{"description":"Removed"}}}},"/api/websites/templates":{"get":{"summary":"List service templates (shared, read-only)","security":[{"bearerAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OrgHeader"}],"responses":{"200":{"description":"OK"}}}},"/api/legal/websites/{websiteId}/policy/publish":{"post":{"summary":"Publish the privacy policy (and with it the imprint) as a new immutable version","description":"Needs the customer's confirmation that they checked privacy policy and imprint, know the generator does not examine their individual case, and are responsible for correctness, completeness and currency. The version stores who confirmed, when and the wording version (`confirmation`). 422 with `details.blockers` while anything the text would state is missing.","security":[{"bearerAuth":[]}],"parameters":[{"name":"websiteId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LegalPublishConfirmation"}}}},"responses":{"201":{"description":"Published; the version summary with `confirmation: { confirmedBy, confirmedAt, textVersion }`"},"400":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"}}}},"/api/legal/websites/{websiteId}/policy/rollback":{"post":{"summary":"Put a stored version live again","description":"A rollback puts a text live, so it needs the same confirmation as a publish. Stored versions are immutable: the record (version, confirmedBy, confirmedAt, textVersion) is kept on the policy and returned as `rollback`.","security":[{"bearerAuth":[]}],"parameters":[{"name":"websiteId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrgHeader"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/LegalPublishConfirmation"},{"type":"object","properties":{"version":{"type":"integer","minimum":1}},"required":["version"]}]}}}},"responses":{"200":{"description":"Live again; the version summary plus `rollback: { version, confirmedBy, confirmedAt, textVersion }`"},"400":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"422":{"$ref":"#/components/responses/Error"}}}},"/runtime/settings/{settingsId}":{"get":{"summary":"Public runtime settings endpoint (consumed by runtime.js)","parameters":[{"name":"settingsId","in":"path","required":true,"schema":{"type":"string"}},{"name":"preview","in":"query","required":false,"schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"Settings payload. Cache-Control: no-store without an ETag — every page load fetches it, and each GET is counted as a pageview","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"httpConsentCookie":{"$ref":"#/components/schemas/HttpConsentCookieRuntime"},"visitor":{"type":"object","required":["countryCode","source"],"properties":{"countryCode":{"type":["string","null"],"pattern":"^[A-Z]{2}$"},"source":{"type":"string","enum":["trusted","unavailable"]}},"description":"Only authenticated operator-controlled edge headers provide a trusted country. Language and client defaults never do."}}}}}},"404":{"description":"Not found or not published"},"409":{"description":"dpa_required: organization has not accepted the current DPA"},"503":{"description":"dpa_not_configured: operator facts not verified"}}}},"/runtime/consent/update":{"post":{"summary":"A visitor's consent (consumed by runtime.js)","description":"Stored as consent evidence with the server time, the live banner version, the reporting host (Origin, else Referer) checked against the registered domains, the User-Agent header and an IP hash keyed per website. Only a consent is stored: a choice that grants no optional category or offered service of the live banner, `reject_all` (runtime <= 0.4.0) and `impression` (runtime <= 0.3.0) are answered 204, not stored and dispatch no webhook.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentReport"}}}},"responses":{"202":{"description":"Stored"},"204":{"description":"A refusal or impression report of an old cached runtime — accepted, not stored"},"400":{"$ref":"#/components/responses/Error"},"404":{"description":"Unknown or archived settingsId"},"409":{"description":"dpa_required: no current organization DPA; no event is stored or dispatched"},"429":{"$ref":"#/components/responses/Error"},"503":{"description":"dpa_not_configured: operator facts not verified"}}}},"/runtime/consent/init":{"post":{"deprecated":true,"summary":"Banner-shown report of runtime <= 0.3.0 — accepted and ignored","description":"Runtime 0.4.0 reports nothing before a decision. Cached older runtimes still call this; the body is not read and nothing is stored.","responses":{"204":{"description":"Ignored"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/v1/websites":{"get":{"summary":"List websites in the API key's organization (Public API)","security":[{"ApiKeyAuth":[]}],"parameters":[{"$ref":"#/components/parameters/OffsetQuery"},{"$ref":"#/components/parameters/LimitQuery"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"websites":{"type":"array","items":{"$ref":"#/components/schemas/PublicWebsite"}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["websites","pagination"]}}}},"401":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/v1/websites/{id}/analytics/summary":{"get":{"summary":"Consent analytics totals over a time range (Public API)","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/FromQuery"},{"$ref":"#/components/parameters/ToQuery"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalyticsSummary"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/v1/websites/{id}/analytics/timeseries":{"get":{"summary":"Consent analytics daily buckets over a time range (Public API)","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/FromQuery"},{"$ref":"#/components/parameters/ToQuery"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AnalyticsTimeseriesPoint"}}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}},"/api/v1/websites/{id}/scans":{"post":{"summary":"Enqueue a website scan (Public API)","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"startUrl":{"type":"string","format":"uri"},"maxPages":{"type":"integer","minimum":1,"maximum":50000,"description":"Clamped to the active website's share of the org's crawl cycle budget."},"maxDepth":{"type":"integer","minimum":0,"maximum":10}},"required":["startUrl"]}}}},"responses":{"201":{"description":"Scan enqueued","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"]}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"}}}}},"webhooks":{"consent.created":{"post":{"summary":"A visitor made a consent decision","description":"Sent to every enabled endpoint subscribed to consent.created. Delivery records (with this payload) are kept 30 days.","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentCreatedWebhook"}}}},"responses":{"200":{"description":"Any 2xx acknowledges the delivery"}}}}}}