From 30384f525aa7fc6a37a59cd25cb25765616d270f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 13:28:15 +0000 Subject: [PATCH 01/19] Document the account configuration endpoints Covers the endpoints added in CookieChimp#2551, which put the dashboard's account configuration surface on the v1 API: services, consent banners, TCF vendors, scans, integrations, and updating an account's settings. Twenty-two new operations, each with a reference page and a nav entry, plus the schemas for the five new resources. Two things the spec now has to say that it didn't before: - A 403. Editing account settings is admin-only on the web, and the API mirrors that: an account key carries the account's own authority, but a user key only does when that user is an admin. AccountAdminRequired is a new reusable response. - Asynchronous work. A scan is created queued and its report only appears once the status reaches completed, so the scan endpoints say to poll rather than expect a report on the create response. Behaviour that would otherwise surprise a caller is documented on the operation rather than left to be discovered: banners are always created disabled and inherit the last-updated banner's design, integrations are backfilled on first listing, re-adding a removed TCF vendor restores the original record, and only one scan runs at a time per account. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- api-reference/account/update-account.mdx | 3 + .../consent-banners/create-consent-banner.mdx | 3 + .../consent-banners/delete-consent-banner.mdx | 3 + .../duplicate-consent-banner.mdx | 3 + .../consent-banners/get-consent-banner.mdx | 3 + .../consent-banners/list-consent-banners.mdx | 3 + .../consent-banners/update-consent-banner.mdx | 3 + .../integrations/get-integration.mdx | 3 + .../integrations/list-integrations.mdx | 3 + .../integrations/update-integration.mdx | 3 + api-reference/scans/create-scan.mdx | 3 + api-reference/scans/get-scan.mdx | 3 + api-reference/scans/list-scans.mdx | 3 + api-reference/services/create-service.mdx | 3 + api-reference/services/delete-service.mdx | 3 + api-reference/services/get-service.mdx | 3 + api-reference/services/list-services.mdx | 3 + api-reference/services/update-service.mdx | 3 + .../tcf-vendors/create-tcf-vendor.mdx | 3 + .../tcf-vendors/delete-tcf-vendor.mdx | 3 + api-reference/tcf-vendors/get-tcf-vendor.mdx | 3 + .../tcf-vendors/list-tcf-vendors.mdx | 3 + docs.json | 49 +- openapi.yaml | 3159 ++++++++++++----- 24 files changed, 2334 insertions(+), 940 deletions(-) create mode 100644 api-reference/account/update-account.mdx create mode 100644 api-reference/consent-banners/create-consent-banner.mdx create mode 100644 api-reference/consent-banners/delete-consent-banner.mdx create mode 100644 api-reference/consent-banners/duplicate-consent-banner.mdx create mode 100644 api-reference/consent-banners/get-consent-banner.mdx create mode 100644 api-reference/consent-banners/list-consent-banners.mdx create mode 100644 api-reference/consent-banners/update-consent-banner.mdx create mode 100644 api-reference/integrations/get-integration.mdx create mode 100644 api-reference/integrations/list-integrations.mdx create mode 100644 api-reference/integrations/update-integration.mdx create mode 100644 api-reference/scans/create-scan.mdx create mode 100644 api-reference/scans/get-scan.mdx create mode 100644 api-reference/scans/list-scans.mdx create mode 100644 api-reference/services/create-service.mdx create mode 100644 api-reference/services/delete-service.mdx create mode 100644 api-reference/services/get-service.mdx create mode 100644 api-reference/services/list-services.mdx create mode 100644 api-reference/services/update-service.mdx create mode 100644 api-reference/tcf-vendors/create-tcf-vendor.mdx create mode 100644 api-reference/tcf-vendors/delete-tcf-vendor.mdx create mode 100644 api-reference/tcf-vendors/get-tcf-vendor.mdx create mode 100644 api-reference/tcf-vendors/list-tcf-vendors.mdx diff --git a/api-reference/account/update-account.mdx b/api-reference/account/update-account.mdx new file mode 100644 index 0000000..61ef52e --- /dev/null +++ b/api-reference/account/update-account.mdx @@ -0,0 +1,3 @@ +--- +openapi: patch /accounts/{id} +--- diff --git a/api-reference/consent-banners/create-consent-banner.mdx b/api-reference/consent-banners/create-consent-banner.mdx new file mode 100644 index 0000000..7d97a12 --- /dev/null +++ b/api-reference/consent-banners/create-consent-banner.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /consent-banners +--- diff --git a/api-reference/consent-banners/delete-consent-banner.mdx b/api-reference/consent-banners/delete-consent-banner.mdx new file mode 100644 index 0000000..7c5033c --- /dev/null +++ b/api-reference/consent-banners/delete-consent-banner.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /consent-banners/{id} +--- diff --git a/api-reference/consent-banners/duplicate-consent-banner.mdx b/api-reference/consent-banners/duplicate-consent-banner.mdx new file mode 100644 index 0000000..2122b8d --- /dev/null +++ b/api-reference/consent-banners/duplicate-consent-banner.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /consent-banners/{id}/duplicate +--- diff --git a/api-reference/consent-banners/get-consent-banner.mdx b/api-reference/consent-banners/get-consent-banner.mdx new file mode 100644 index 0000000..1086736 --- /dev/null +++ b/api-reference/consent-banners/get-consent-banner.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /consent-banners/{id} +--- diff --git a/api-reference/consent-banners/list-consent-banners.mdx b/api-reference/consent-banners/list-consent-banners.mdx new file mode 100644 index 0000000..e098ab2 --- /dev/null +++ b/api-reference/consent-banners/list-consent-banners.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /consent-banners +--- diff --git a/api-reference/consent-banners/update-consent-banner.mdx b/api-reference/consent-banners/update-consent-banner.mdx new file mode 100644 index 0000000..c86f46e --- /dev/null +++ b/api-reference/consent-banners/update-consent-banner.mdx @@ -0,0 +1,3 @@ +--- +openapi: put /consent-banners/{id} +--- diff --git a/api-reference/integrations/get-integration.mdx b/api-reference/integrations/get-integration.mdx new file mode 100644 index 0000000..010b383 --- /dev/null +++ b/api-reference/integrations/get-integration.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /integrations/{id} +--- diff --git a/api-reference/integrations/list-integrations.mdx b/api-reference/integrations/list-integrations.mdx new file mode 100644 index 0000000..8bda06e --- /dev/null +++ b/api-reference/integrations/list-integrations.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /integrations +--- diff --git a/api-reference/integrations/update-integration.mdx b/api-reference/integrations/update-integration.mdx new file mode 100644 index 0000000..97d231b --- /dev/null +++ b/api-reference/integrations/update-integration.mdx @@ -0,0 +1,3 @@ +--- +openapi: put /integrations/{id} +--- diff --git a/api-reference/scans/create-scan.mdx b/api-reference/scans/create-scan.mdx new file mode 100644 index 0000000..4c53a99 --- /dev/null +++ b/api-reference/scans/create-scan.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /scans +--- diff --git a/api-reference/scans/get-scan.mdx b/api-reference/scans/get-scan.mdx new file mode 100644 index 0000000..3c53c07 --- /dev/null +++ b/api-reference/scans/get-scan.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /scans/{id} +--- diff --git a/api-reference/scans/list-scans.mdx b/api-reference/scans/list-scans.mdx new file mode 100644 index 0000000..d7c5c07 --- /dev/null +++ b/api-reference/scans/list-scans.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /scans +--- diff --git a/api-reference/services/create-service.mdx b/api-reference/services/create-service.mdx new file mode 100644 index 0000000..feda61d --- /dev/null +++ b/api-reference/services/create-service.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /services +--- diff --git a/api-reference/services/delete-service.mdx b/api-reference/services/delete-service.mdx new file mode 100644 index 0000000..3d3da3c --- /dev/null +++ b/api-reference/services/delete-service.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /services/{id} +--- diff --git a/api-reference/services/get-service.mdx b/api-reference/services/get-service.mdx new file mode 100644 index 0000000..b3affef --- /dev/null +++ b/api-reference/services/get-service.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /services/{id} +--- diff --git a/api-reference/services/list-services.mdx b/api-reference/services/list-services.mdx new file mode 100644 index 0000000..77ae155 --- /dev/null +++ b/api-reference/services/list-services.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /services +--- diff --git a/api-reference/services/update-service.mdx b/api-reference/services/update-service.mdx new file mode 100644 index 0000000..61ddf80 --- /dev/null +++ b/api-reference/services/update-service.mdx @@ -0,0 +1,3 @@ +--- +openapi: put /services/{id} +--- diff --git a/api-reference/tcf-vendors/create-tcf-vendor.mdx b/api-reference/tcf-vendors/create-tcf-vendor.mdx new file mode 100644 index 0000000..fe0a612 --- /dev/null +++ b/api-reference/tcf-vendors/create-tcf-vendor.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /tcf-vendors +--- diff --git a/api-reference/tcf-vendors/delete-tcf-vendor.mdx b/api-reference/tcf-vendors/delete-tcf-vendor.mdx new file mode 100644 index 0000000..d2b5bc0 --- /dev/null +++ b/api-reference/tcf-vendors/delete-tcf-vendor.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /tcf-vendors/{id} +--- diff --git a/api-reference/tcf-vendors/get-tcf-vendor.mdx b/api-reference/tcf-vendors/get-tcf-vendor.mdx new file mode 100644 index 0000000..fed7990 --- /dev/null +++ b/api-reference/tcf-vendors/get-tcf-vendor.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /tcf-vendors/{id} +--- diff --git a/api-reference/tcf-vendors/list-tcf-vendors.mdx b/api-reference/tcf-vendors/list-tcf-vendors.mdx new file mode 100644 index 0000000..56d1dec --- /dev/null +++ b/api-reference/tcf-vendors/list-tcf-vendors.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /tcf-vendors +--- diff --git a/docs.json b/docs.json index b2274c4..2e3296e 100644 --- a/docs.json +++ b/docs.json @@ -163,7 +163,54 @@ "group": "Account", "pages": [ "api-reference/account/list-accounts", - "api-reference/account/get-account" + "api-reference/account/get-account", + "api-reference/account/update-account" + ] + }, + { + "group": "Consent Banners", + "pages": [ + "api-reference/consent-banners/list-consent-banners", + "api-reference/consent-banners/create-consent-banner", + "api-reference/consent-banners/get-consent-banner", + "api-reference/consent-banners/update-consent-banner", + "api-reference/consent-banners/duplicate-consent-banner", + "api-reference/consent-banners/delete-consent-banner" + ] + }, + { + "group": "Services", + "pages": [ + "api-reference/services/list-services", + "api-reference/services/create-service", + "api-reference/services/get-service", + "api-reference/services/update-service", + "api-reference/services/delete-service" + ] + }, + { + "group": "TCF Vendors", + "pages": [ + "api-reference/tcf-vendors/list-tcf-vendors", + "api-reference/tcf-vendors/create-tcf-vendor", + "api-reference/tcf-vendors/get-tcf-vendor", + "api-reference/tcf-vendors/delete-tcf-vendor" + ] + }, + { + "group": "Scans", + "pages": [ + "api-reference/scans/list-scans", + "api-reference/scans/create-scan", + "api-reference/scans/get-scan" + ] + }, + { + "group": "Integrations", + "pages": [ + "api-reference/integrations/list-integrations", + "api-reference/integrations/get-integration", + "api-reference/integrations/update-integration" ] }, { diff --git a/openapi.yaml b/openapi.yaml index dfcabe6..c912e77 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -704,6 +704,48 @@ paths: "429": $ref: "#/components/responses/RateLimited" + patch: + summary: Update account + description: | + Update an account's settings — the same fields the dashboard's account + settings screens expose. + + An account token carries the account's own authority. A user token must + belong to an admin of the account, mirroring the web UI, and a + non-admin member gets a `403`. + + `account_type` cannot be changed here, as on the web form: mobile app + accounts are only created through the admin panel. + operationId: updateAccount + tags: + - Accounts + parameters: + - $ref: "#/components/parameters/id" + requestBody: + description: Account settings to update + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/AccountWriteRequest" + responses: + "200": + description: Updated account + content: + application/json: + schema: + $ref: "#/components/schemas/Account" + "401": + $ref: "#/components/responses/Unauthorized" + "403": + $ref: "#/components/responses/AccountAdminRequired" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" + # account user endpoints - index, GET, PUT & DELETE /account-users: get: @@ -974,1189 +1016,2428 @@ paths: "429": $ref: "#/components/responses/RateLimited" -components: - securitySchemes: - authorization: - type: http - scheme: bearer - description: API token obtained from the login endpoint or the dashboard - - parameters: - id: - name: id - in: path - description: ID of the resource - required: true - schema: - type: string - page: - name: page - in: query - description: | - Page number. Must be a positive integer; anything else returns a `400`. - Requesting a page past the end returns the last page. - required: false - schema: - type: integer - minimum: 1 - default: 1 - per_page: - name: per_page - in: query - description: | - Number of records to return per page. The effective range is 1–50; any - value outside it is accepted but falls back to the default of 20. - required: false - schema: - type: integer - default: 20 - onboarding_status: - name: onboarding_status - in: query - description: | - The status of the account, where: - - `account_created`: The account has been created. - - `vendors_setup`: Categories, services or TCF vendors have been set up. - - `code_installed`: The script has been installed on the user's website. - - `inactive`: Less than 10 visitors to the user's website in the last 30 days. - - `live`: The account is live and operational. - required: false - schema: - type: string - enum: [account_created, vendors_setup, code_installed, inactive, live] - visitor_stats: - name: visitor_stats - in: query + # site service endpoints - index, GET, POST, PUT & DELETE + /services: + get: + summary: List services description: | - Set to `false` to skip resolving analytics and return `visitor_stats` as - `null`. Useful when you only need the account list, as it avoids the - analytics lookup entirely. - required: false - schema: - type: boolean - default: true + List the services configured on the current account, in name order. + Requires an account-scoped API token. + operationId: listServices + tags: + - Services + parameters: + - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/page" + responses: + "200": + description: List of services + content: + application/json: + schema: + type: object + properties: + pagination: + $ref: "#/components/schemas/BasePagination" + data: + type: array + items: + $ref: "#/components/schemas/SiteService" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "429": + $ref: "#/components/responses/RateLimited" - responses: - BadRequest: - description: Invalid pagination parameter - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - example: - errors: - - code: 400 - message: "Invalid page parameter" + post: + summary: Create service + description: | + Create a service under one of the account's categories. `category_id` + is required and must belong to this account. + operationId: createService + tags: + - Services + requestBody: + description: Service to create + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SiteServiceWriteRequest" + responses: + "201": + description: Created service + content: + application/json: + schema: + $ref: "#/components/schemas/SiteServiceResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + description: The referenced `category_id` does not belong to this account + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" - Unauthorized: - description: Missing, invalid or expired API token - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - example: - errors: - - code: 401 - message: "Invalid API Key" + /services/{id}: + get: + summary: Get service + description: Get service by ID + operationId: getService + tags: + - Services + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Service + content: + application/json: + schema: + $ref: "#/components/schemas/SiteServiceResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/RateLimited" - AccountTokenRequired: + put: + summary: Update service description: | - Missing or invalid API token, or a token that does not resolve to an - account. Account-scoped endpoints accept an account token, or a user - token whose user belongs to an account; partner tokens are rejected. - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - example: - errors: - - code: 401 - message: "API key must belong to an account" - - PartnerTokenRequired: - description: Missing or invalid API token, or a token that does not belong to a partner - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - example: - errors: - - code: 401 - message: "API key must belong to a partner" + Update a service. Passing `category_id` moves the service to that + category; omitting it leaves the service where it is. + operationId: updateService + tags: + - Services + parameters: + - $ref: "#/components/parameters/id" + requestBody: + description: Service to update + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/SiteServiceWriteRequest" + responses: + "200": + description: Updated service + content: + application/json: + schema: + $ref: "#/components/schemas/SiteServiceResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" - NotFound: - description: The record does not exist, or does not belong to the caller - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - example: - errors: - - code: 404 - message: "Record not found" + delete: + summary: Delete service + description: Delete a service. It is soft-deleted and stops appearing in listings. + operationId: deleteService + tags: + - Services + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Service deleted + content: + application/json: + schema: + $ref: "#/components/schemas/DeletedMessage" + example: + message: "Service was deleted successfully" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/RateLimited" - RateLimited: + # consent banner endpoints - index, GET, POST, PUT, DELETE & duplicate + /consent-banners: + get: + summary: List consent banners description: | - Too many requests. The general limit is 300 requests per 5 minutes per - API token; the auth endpoints are throttled more tightly per IP. - headers: - Retry-After: - description: Seconds until the current rate-limit window resets - schema: - type: integer - content: - application/json: - schema: - $ref: "#/components/schemas/Error" - example: - errors: + List the account's consent banners, newest first. Requires an + account-scoped API token. + operationId: listConsentBanners + tags: + - Consent Banners + parameters: + - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/page" + responses: + "200": + description: List of consent banners + content: + application/json: + schema: + type: object + properties: + pagination: + $ref: "#/components/schemas/BasePagination" + data: + type: array + items: + $ref: "#/components/schemas/ConsentBanner" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "429": + $ref: "#/components/responses/RateLimited" + + post: + summary: Create consent banner + description: | + Create a consent banner. It inherits the design of the account's most + recently updated banner, the same way the dashboard's new-banner flow + does, and is always created disabled — enable it once its content and + targeting are set. A name is generated if you don't supply one. + operationId: createConsentBanner + tags: + - Consent Banners + requestBody: + description: Consent banner to create + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ConsentBannerWriteRequest" + responses: + "201": + description: Created consent banner + content: + application/json: + schema: + $ref: "#/components/schemas/ConsentBannerResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" + + /consent-banners/{id}: + get: + summary: Get consent banner + description: Get consent banner by ID + operationId: getConsentBanner + tags: + - Consent Banners + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Consent banner + content: + application/json: + schema: + $ref: "#/components/schemas/ConsentBannerResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/RateLimited" + + put: + summary: Update consent banner + description: | + Update a consent banner's content, appearance or targeting. Set + `enabled` to `true` to make it live. + operationId: updateConsentBanner + tags: + - Consent Banners + parameters: + - $ref: "#/components/parameters/id" + requestBody: + description: Consent banner to update + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ConsentBannerWriteRequest" + responses: + "200": + description: Updated consent banner + content: + application/json: + schema: + $ref: "#/components/schemas/ConsentBannerResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" + + delete: + summary: Delete consent banner + description: Delete a consent banner. It is soft-deleted and stops appearing in listings. + operationId: deleteConsentBanner + tags: + - Consent Banners + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Consent banner deleted + content: + application/json: + schema: + $ref: "#/components/schemas/DeletedMessage" + example: + message: "Consent banner was deleted successfully" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/RateLimited" + + /consent-banners/{id}/duplicate: + post: + summary: Duplicate consent banner + description: | + Copy a consent banner, including its design and targeting. The copy is + named after the original and is always created disabled. + operationId: duplicateConsentBanner + tags: + - Consent Banners + parameters: + - $ref: "#/components/parameters/id" + responses: + "201": + description: The duplicated consent banner + content: + application/json: + schema: + $ref: "#/components/schemas/ConsentBannerResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" + + # integration endpoints - index, GET & PUT + /integrations: + get: + summary: List integrations + description: | + List every integration available to the account, active and inactive. + Integrations the account has never opened are created inactive on first + listing, so the collection is always the full set you can enable. + operationId: listIntegrations + tags: + - Integrations + parameters: + - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/page" + responses: + "200": + description: List of integrations + content: + application/json: + schema: + type: object + properties: + pagination: + $ref: "#/components/schemas/BasePagination" + data: + type: array + items: + $ref: "#/components/schemas/Integration" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "429": + $ref: "#/components/responses/RateLimited" + + /integrations/{id}: + get: + summary: Get integration + description: Get integration by ID + operationId: getIntegration + tags: + - Integrations + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Integration + content: + application/json: + schema: + $ref: "#/components/schemas/IntegrationResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/RateLimited" + + put: + summary: Update integration + description: | + Enable, disable or configure an integration. Every integration accepts + `status`; the rest of the accepted keys depend on which integration it + is, and anything else is ignored. + + - `SiteScanIntegration` also accepts `scan_frequency` (`weekly` or + `monthly`; weekly is not available on free accounts) and + `auto_add_vendors`. + - `GoogleConsentModeIntegration` also accepts `url_passthrough` and + `ads_data_redaction`. + + Enabling an integration may add its associated vendors to the account. + operationId: updateIntegration + tags: + - Integrations + parameters: + - $ref: "#/components/parameters/id" + requestBody: + description: Integration settings to update + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/IntegrationWriteRequest" + responses: + "200": + description: Updated integration + content: + application/json: + schema: + $ref: "#/components/schemas/IntegrationResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" + + # scan endpoints - index, GET & POST + /scans: + get: + summary: List scans + description: | + List the account's website scans, newest first. Requires an + account-scoped API token. + operationId: listScans + tags: + - Scans + parameters: + - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/page" + responses: + "200": + description: List of scans + content: + application/json: + schema: + type: object + properties: + pagination: + $ref: "#/components/schemas/BasePagination" + data: + type: array + items: + $ref: "#/components/schemas/Scan" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "429": + $ref: "#/components/responses/RateLimited" + + post: + summary: Start a scan + description: | + Queue a scan of the account's website. Only one scan can run at a time + per account, and accounts have a daily scan limit — both come back as a + `422`. + + Scanning is asynchronous: the scan is created `queued`, and the report + appears on the scan once its status reaches `completed`. + operationId: createScan + tags: + - Scans + requestBody: + description: Scan to queue + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ScanWriteRequest" + responses: + "201": + description: Queued scan + content: + application/json: + schema: + $ref: "#/components/schemas/ScanResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" + + /scans/{id}: + get: + summary: Get scan + description: | + Get a scan by ID. `report` is only present once the scan has completed; + poll `status` until then. + operationId: getScan + tags: + - Scans + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: Scan + content: + application/json: + schema: + $ref: "#/components/schemas/ScanResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/RateLimited" + + # TCF vendor endpoints - index, GET, POST & DELETE + /tcf-vendors: + get: + summary: List TCF vendors + description: | + List the IAB TCF vendors enabled on the account, in global vendor ID + order. Requires an account-scoped API token. + operationId: listTcfVendors + tags: + - TCF Vendors + parameters: + - $ref: "#/components/parameters/per_page" + - $ref: "#/components/parameters/page" + responses: + "200": + description: List of TCF vendors + content: + application/json: + schema: + type: object + properties: + pagination: + $ref: "#/components/schemas/BasePagination" + data: + type: array + items: + $ref: "#/components/schemas/TcfVendor" + "400": + $ref: "#/components/responses/BadRequest" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "429": + $ref: "#/components/responses/RateLimited" + + post: + summary: Add a TCF vendor + description: | + Add a vendor from the IAB Global Vendor List to the account, one at a + time, so you can reconcile against your own list without restating it. + + Re-adding a vendor that was previously removed restores the original + record rather than creating a second one. A vendor already on the + account, or an ID that isn't in the Global Vendor List, returns a `422`. + operationId: createTcfVendor + tags: + - TCF Vendors + requestBody: + description: Vendor to add + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/TcfVendorWriteRequest" + responses: + "201": + description: The added vendor + content: + application/json: + schema: + $ref: "#/components/schemas/TcfVendorResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "422": + $ref: "#/components/responses/ValidationError" + "429": + $ref: "#/components/responses/RateLimited" + + /tcf-vendors/{id}: + get: + summary: Get TCF vendor + description: Get TCF vendor by ID + operationId: getTcfVendor + tags: + - TCF Vendors + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: TCF vendor + content: + application/json: + schema: + $ref: "#/components/schemas/TcfVendorResponse" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/RateLimited" + + delete: + summary: Remove a TCF vendor + description: | + Remove a vendor from the account. The record is soft-deleted, so adding + the vendor again later restores it. + operationId: deleteTcfVendor + tags: + - TCF Vendors + parameters: + - $ref: "#/components/parameters/id" + responses: + "200": + description: TCF vendor removed + content: + application/json: + schema: + $ref: "#/components/schemas/DeletedMessage" + example: + message: "TCF vendor was deleted successfully" + "401": + $ref: "#/components/responses/AccountTokenRequired" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/RateLimited" + +components: + securitySchemes: + authorization: + type: http + scheme: bearer + description: API token obtained from the login endpoint or the dashboard + + parameters: + id: + name: id + in: path + description: ID of the resource + required: true + schema: + type: string + page: + name: page + in: query + description: | + Page number. Must be a positive integer; anything else returns a `400`. + Requesting a page past the end returns the last page. + required: false + schema: + type: integer + minimum: 1 + default: 1 + per_page: + name: per_page + in: query + description: | + Number of records to return per page. The effective range is 1–50; any + value outside it is accepted but falls back to the default of 20. + required: false + schema: + type: integer + default: 20 + onboarding_status: + name: onboarding_status + in: query + description: | + The status of the account, where: + - `account_created`: The account has been created. + - `vendors_setup`: Categories, services or TCF vendors have been set up. + - `code_installed`: The script has been installed on the user's website. + - `inactive`: Less than 10 visitors to the user's website in the last 30 days. + - `live`: The account is live and operational. + required: false + schema: + type: string + enum: [account_created, vendors_setup, code_installed, inactive, live] + visitor_stats: + name: visitor_stats + in: query + description: | + Set to `false` to skip resolving analytics and return `visitor_stats` as + `null`. Useful when you only need the account list, as it avoids the + analytics lookup entirely. + required: false + schema: + type: boolean + default: true + + responses: + BadRequest: + description: Invalid pagination parameter + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + errors: + - code: 400 + message: "Invalid page parameter" + + Unauthorized: + description: Missing, invalid or expired API token + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + errors: + - code: 401 + message: "Invalid API Key" + + AccountTokenRequired: + description: | + Missing or invalid API token, or a token that does not resolve to an + account. Account-scoped endpoints accept an account token, or a user + token whose user belongs to an account; partner tokens are rejected. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + errors: + - code: 401 + message: "API key must belong to an account" + + PartnerTokenRequired: + description: Missing or invalid API token, or a token that does not belong to a partner + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + errors: + - code: 401 + message: "API key must belong to a partner" + + AccountAdminRequired: + description: | + The token resolved to a user who is not an admin of this account. + Account tokens carry the account's own authority and never see this. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + errors: + - code: 403 + message: "User must be an admin of this account" + + NotFound: + description: The record does not exist, or does not belong to the caller + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + errors: + - code: 404 + message: "Record not found" + + RateLimited: + description: | + Too many requests. The general limit is 300 requests per 5 minutes per + API token; the auth endpoints are throttled more tightly per IP. + headers: + Retry-After: + description: Seconds until the current rate-limit window resets + schema: + type: integer + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + errors: - code: 429 message: "Too many requests. Please retry later." - ValidationError: - description: The record could not be saved - content: - application/json: - schema: - $ref: "#/components/schemas/ValidationError" + ValidationError: + description: The record could not be saved + content: + application/json: + schema: + $ref: "#/components/schemas/ValidationError" + + ModelValidationError: + description: The record could not be saved + content: + application/json: + schema: + $ref: "#/components/schemas/ModelValidationError" + + schemas: + Error: + type: object + description: | + The standard error envelope, used for authentication, authorization, + pagination and not-found errors. + properties: + errors: + type: array + items: + type: object + properties: + code: + type: integer + message: + type: string + example: + errors: + - code: 401 + message: "Invalid API Key" + + ValidationError: + type: object + description: | + Returned when a create or update fails validation. Each entry carries a + single human-readable `error` string. + properties: + errors: + type: array + items: + type: object + properties: + error: + type: string + example: + errors: + - error: "Name can't be blank" + + ModelValidationError: + type: object + description: | + Returned by group create/update and account-user update, which render + the model's errors directly: an object keyed by attribute name. Other + validation failures — including removing an account owner — use + `ValidationError`. + additionalProperties: + type: array + items: + type: string + example: + name: ["can't be blank"] + external_id: ["has already been taken"] + + MessageError: + type: object + description: Returned by the auth and password endpoints, which use a single `error` string. + properties: + error: + type: string + example: + error: "Invalid Email or password." + + DeletedMessage: + type: object + properties: + message: + type: string + example: + message: "Record was deleted successfully" + + BasePagination: + type: object + properties: + prev_url: + type: string + description: Path to get the previous page + next_url: + type: string + description: Path to get the next page + count: + type: integer + description: Total number of records across all pages + page: + type: integer + description: Current page + prev: + type: [integer, "null"] + description: Previous page number, or null on the first page + next: + type: [integer, "null"] + description: Next page number, or null on the last page + + AccountPagination: + allOf: + - $ref: "#/components/schemas/BasePagination" + example: + prev_url: "/api/v1/accounts?page=" + next_url: "/api/v1/accounts?page=2" + count: 68 + page: 1 + prev: null + next: 2 + + AccountInvitationPagination: + allOf: + - $ref: "#/components/schemas/BasePagination" + example: + prev_url: "/api/v1/account-invitations?page=" + next_url: "/api/v1/account-invitations?page=2" + count: 68 + page: 1 + prev: null + next: 2 + + AccountUserPagination: + allOf: + - $ref: "#/components/schemas/BasePagination" + example: + prev_url: "/api/v1/account-users?page=" + next_url: "/api/v1/account-users?page=2" + count: 68 + page: 1 + prev: null + next: 2 + + AIActionPagination: + allOf: + - $ref: "#/components/schemas/BasePagination" + example: + prev_url: "/api/v1/ai-actions?page=" + next_url: "/api/v1/ai-actions?page=2" + count: 68 + page: 1 + prev: null + next: 2 + + CategoryPagination: + allOf: + - $ref: "#/components/schemas/BasePagination" + example: + prev_url: "/api/v1/categories?page=" + next_url: "/api/v1/categories?page=2" + count: 68 + page: 1 + prev: null + next: 2 + + ConsentPagination: + allOf: + - $ref: "#/components/schemas/BasePagination" + example: + prev_url: "/api/v1/consents?page=" + next_url: "/api/v1/consents?page=2" + count: 68 + page: 1 + prev: null + next: 2 + + GroupPagination: + allOf: + - $ref: "#/components/schemas/BasePagination" + example: + prev_url: "/api/v1/groups?page=" + next_url: "/api/v1/groups?page=2" + count: 68 + page: 1 + prev: null + next: 2 + + StorageItemPagination: + allOf: + - $ref: "#/components/schemas/BasePagination" + example: + prev_url: "/api/v1/storage-items?page=" + next_url: "/api/v1/storage-items?page=2" + count: 68 + page: 1 + prev: null + next: 2 + + LoginRequest: + type: object + properties: + email: + type: string + format: email + description: The user's email address + password: + type: string + format: password + description: The user's password + required: + - email + - password + example: + email: "user@example.com" + password: "correct-horse-battery-staple" + + LoginResponse: + type: object + properties: + token: + type: string + description: API token to send as a bearer token on subsequent requests + example: + token: "0mHFuvY2mCVfMBXbtNCPXA" + + PasswordUpdateRequest: + type: object + properties: + user: + type: object + properties: + current_password: + type: string + format: password + password: + type: string + format: password + password_confirmation: + type: string + format: password + required: + - current_password + - password + - password_confirmation + required: + - user + example: + user: + current_password: "correct-horse-battery-staple" + password: "a-longer-new-password" + password_confirmation: "a-longer-new-password" + + CategoryWrite: + type: object + properties: + name: + type: string + description: Name of the category that is displayed to users + slug: + type: string + description: | + URL-friendly identifier for the category, used by the consent banner + and the JavaScript SDK. Some slugs are reserved by CookieChimp. + description: + type: string + description: Description of the category that is displayed to users + required: + type: boolean + description: Whether the category is required for the website to function + required: + - name + + CategoryWriteRequest: + type: object + properties: + category: + $ref: "#/components/schemas/CategoryWrite" + required: + - category + example: + category: + name: "Analytics" + slug: "analytics" + description: "Cookies that help us understand how the site is used" + required: false + + Category: + type: object + properties: + id: + type: string + readOnly: true + description: Unique identifier for the category + name: + type: string + description: Name of the category that is displayed to users + description: + type: string + description: Description of the category that is displayed to users + slug: + type: string + description: URL-friendly identifier for the category + required: + type: boolean + description: Whether the category is required for the website to function + default_language: + type: string + description: Language code the name and description are written in + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + example: + id: "essential" + name: "Essential" + description: "Cookies essential for website functionality" + slug: "essential" + required: true + default_language: "en" + created_at: "2023-01-01T00:00:00Z" + updated_at: "2023-01-02T00:00:00Z" + + CategoryResponse: + type: object + properties: + data: + $ref: "#/components/schemas/Category" + + StorageItemWrite: + type: object + properties: + name: + type: string + description: Name of the storage item + storage_type: + type: string + enum: [cookie, local_storage, session_storage] + description: The kind of browser storage this item uses + description: + type: string + description: Description of the storage item that is displayed to users + company_name: + type: string + description: Name of the company that owns the storage item + domain: + type: string + description: Domain of the storage item + duration: + type: string + description: How long the storage item persists + partitioned: + type: boolean + description: Whether the storage item is partitioned (CHIPS) + path: + type: string + description: Path of the storage item + samesite: + type: string + enum: [lax, strict] + description: SameSite attribute of the storage item + category_id: + type: string + description: ID of the category to file this item under + site_service_id: + type: string + description: | + ID of the service this item belongs to. When set, the item inherits + the service's category. + required: + - name + + StorageItemWriteRequest: + type: object + properties: + storage_item: + $ref: "#/components/schemas/StorageItemWrite" + required: + - storage_item + example: + storage_item: + name: "_ga" + storage_type: "cookie" + description: "Used to distinguish users." + company_name: "Google" + domain: ".example.com" + duration: "2 years" + partitioned: false + path: "/" + samesite: "lax" + category_id: "cat1" + + StorageItem: + type: object + properties: + id: + type: string + readOnly: true + description: Unique identifier for the storage item + name: + type: string + storage_type: + type: string + enum: [cookie, local_storage, session_storage] + category: + type: object + description: | + The category this item is filed under. Falls back to the category of + the item's service when the item has no category of its own. + properties: + id: + type: [string, "null"] + name: + type: [string, "null"] + service: + type: object + description: The service this item belongs to, if any + properties: + id: + type: [string, "null"] + name: + type: [string, "null"] + description: + type: string + company_name: + type: string + duration: + type: string + domain: + type: string + partitioned: + type: boolean + path: + type: string + samesite: + type: [string, "null"] + enum: [lax, strict, null] + wildcard_id: + type: [string, "null"] + readOnly: true + description: ID of the wildcard item this one was matched against, if any + default_language: + type: string + description: Language code the name and description are written in + last_seen_at: + type: [string, "null"] + format: date-time + readOnly: true + description: When this item was last observed on the website + created_by: + type: string + enum: [user, scan] + readOnly: true + description: Whether the item was added by a user or found by a scan + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + example: + id: "sto123" + name: "_ga" + storage_type: "cookie" + category: + id: "cat1" + name: "Analytics" + service: + id: "svc1" + name: "Google Analytics" + description: "Used to distinguish users." + company_name: "Google" + duration: "2 years" + domain: ".example.com" + partitioned: false + path: "/" + samesite: "lax" + wildcard_id: null + default_language: "en" + last_seen_at: "2023-03-23T12:34:56Z" + created_by: "scan" + created_at: "2023-03-01T09:00:00Z" + updated_at: "2023-03-23T12:34:56Z" + + StorageItemResponse: + type: object + properties: + data: + $ref: "#/components/schemas/StorageItem" - ModelValidationError: - description: The record could not be saved - content: - application/json: - schema: - $ref: "#/components/schemas/ModelValidationError" + GroupCreate: + type: object + properties: + name: + type: string + description: Name of the group + external_id: + type: string + description: External ID set by the partner + max_pageviews_per_month: + type: integer + description: Maximum number of pageviews per month + max_unique_visitors_per_month: + type: integer + description: Maximum number of unique visitors per month + required: + - name + - external_id - schemas: - Error: + GroupCreateRequest: type: object - description: | - The standard error envelope, used for authentication, authorization, - pagination and not-found errors. properties: - errors: - type: array - items: - type: object - properties: - code: - type: integer - message: - type: string + group: + $ref: "#/components/schemas/GroupCreate" + required: + - group example: - errors: - - code: 401 - message: "Invalid API Key" + group: + name: "Example Ltd" + external_id: "ABC_123" + max_pageviews_per_month: 500000 + max_unique_visitors_per_month: 100000 - ValidationError: + GroupUpdate: + type: object + properties: + name: + type: string + description: Name of the group + external_id: + type: string + description: External ID set by the partner + required: + - name + - external_id + + GroupUpdateRequest: + type: object + properties: + group: + $ref: "#/components/schemas/GroupUpdate" + required: + - group + example: + group: + name: "Example Ltd" + external_id: "ABC_123" + + Group: + type: object + properties: + id: + type: string + readOnly: true + description: Unique identifier for the group + partner: + type: string + readOnly: true + description: Name of the partner the group belongs to + name: + type: string + description: Name of the group + external_id: + type: string + description: External ID set by the partner + max_pageviews_per_month: + type: [integer, "null"] + description: Maximum number of pageviews per month + max_unique_visitors_per_month: + type: [integer, "null"] + description: Maximum number of unique visitors per month + example: + id: "abc123" + partner: "partner_one" + name: "Example Ltd" + external_id: "ABC_123" + max_pageviews_per_month: 500000 + max_unique_visitors_per_month: 100000 + + Consent: type: object description: | - Returned when a create or update fails validation. Each entry carries a - single human-readable `error` string. + A consent record, written when a visitor interacts with the consent + banner. properties: - errors: - type: array + id: + type: string + description: Unique identifier for the consent + consent_banner_id: + type: [string, "null"] + description: ID of the consent banner the visitor saw + uuid: + type: string + description: Anonymous visitor identifier generated by the widget + url: + type: string + description: URL the consent was given on + ip_address: + type: [string, "null"] + description: Partially masked IP address of the visitor + ip_country: + type: [string, "null"] + description: Two-letter country code resolved from the visitor's IP + ip_region: + type: [string, "null"] + description: Region resolved from the visitor's IP + visitor_user_id: + type: [string, "null"] + description: ID of the user set by your website + accept_type: + type: [string, "null"] + description: | + How the visitor responded. Typically `all`, `custom` or `necessary`. + accepted_categories: + type: [array, "null"] + items: + type: string + description: Slugs of the categories the visitor accepted + rejected_categories: + type: [array, "null"] + items: + type: string + description: Slugs of the categories the visitor rejected + accepted_services: + type: [object, "null"] + additionalProperties: + type: array + items: + type: string + description: Accepted services, keyed by category slug + rejected_services: + type: [object, "null"] + additionalProperties: + type: array + items: + type: string + description: Rejected services, keyed by category slug + accepted_cookies: + type: [array, "null"] items: type: object - properties: - error: - type: string - example: - errors: - - error: "Name can't be blank" - - ModelValidationError: - type: object - description: | - Returned by group create/update and account-user update, which render - the model's errors directly: an object keyed by attribute name. Other - validation failures — including removing an account owner — use - `ValidationError`. - additionalProperties: - type: array - items: + description: Storage items the visitor accepted + rejected_cookies: + type: [array, "null"] + items: + type: object + description: Storage items the visitor rejected + device: type: string - example: - name: ["can't be blank"] - external_id: ["has already been taken"] - - MessageError: - type: object - description: Returned by the auth and password endpoints, which use a single `error` string. - properties: - error: + description: Device type derived from the user agent + user_agent: + type: string + description: User agent of the visitor's browser + gpc_preference: + type: [boolean, "null"] + description: Whether the visitor sent a Global Privacy Control signal + do_not_sell: + type: boolean + description: Whether the visitor opted out of the sale of their data + created_at: type: string + format: date-time + description: Date and time when the consent was recorded example: - error: "Invalid Email or password." + id: "consent321" + consent_banner_id: "banner123" + uuid: "8f14e45f-ea8f-4f2e-b1b0-0f1d2c3b4a59" + url: "https://example.com/pricing" + ip_address: "203.0.113.XXX" + ip_country: "GB" + ip_region: "England" + visitor_user_id: "user456" + accept_type: "custom" + accepted_categories: ["essential", "analytics"] + rejected_categories: ["marketing"] + accepted_services: { "analytics": ["Google Analytics"] } + rejected_services: { "marketing": ["Meta Pixel"] } + accepted_cookies: [] + rejected_cookies: [] + device: "desktop" + user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" + gpc_preference: false + do_not_sell: false + created_at: "2023-01-20T12:46:30Z" - DeletedMessage: + ConsentResponse: type: object properties: - message: - type: string - example: - message: "Record was deleted successfully" + data: + $ref: "#/components/schemas/Consent" - BasePagination: + AIAction: type: object + description: An action CookieChimp's AI took on a category or storage item. properties: - prev_url: + id: type: string - description: Path to get the previous page - next_url: + description: Unique identifier for the AI action + action_type: type: string - description: Path to get the next page - count: - type: integer - description: Total number of records across all pages - page: - type: integer - description: Current page - prev: - type: [integer, "null"] - description: Previous page number, or null on the first page - next: - type: [integer, "null"] - description: Next page number, or null on the last page - - AccountPagination: - allOf: - - $ref: "#/components/schemas/BasePagination" - example: - prev_url: "/api/v1/accounts?page=" - next_url: "/api/v1/accounts?page=2" - count: 68 - page: 1 - prev: null - next: 2 - - AccountInvitationPagination: - allOf: - - $ref: "#/components/schemas/BasePagination" - example: - prev_url: "/api/v1/account-invitations?page=" - next_url: "/api/v1/account-invitations?page=2" - count: 68 - page: 1 - prev: null - next: 2 - - AccountUserPagination: - allOf: - - $ref: "#/components/schemas/BasePagination" - example: - prev_url: "/api/v1/account-users?page=" - next_url: "/api/v1/account-users?page=2" - count: 68 - page: 1 - prev: null - next: 2 - - AIActionPagination: - allOf: - - $ref: "#/components/schemas/BasePagination" - example: - prev_url: "/api/v1/ai-actions?page=" - next_url: "/api/v1/ai-actions?page=2" - count: 68 - page: 1 - prev: null - next: 2 - - CategoryPagination: - allOf: - - $ref: "#/components/schemas/BasePagination" - example: - prev_url: "/api/v1/categories?page=" - next_url: "/api/v1/categories?page=2" - count: 68 - page: 1 - prev: null - next: 2 - - ConsentPagination: - allOf: - - $ref: "#/components/schemas/BasePagination" + enum: [autofill, auto_categorise, translate] + description: What the AI did + actionable_type: + type: string + enum: [Category, ClientStorageItem] + description: The type of record the action was taken on + actionable_id: + type: string + description: ID of the record the action was taken on + suggested_params: + type: [object, "null"] + additionalProperties: true + description: The values the AI suggested + created_at: + type: string + format: date-time + actionable: + description: | + The record the action was taken on — a category or a storage item, + in the same shape those endpoints return. Use `actionable_type` to + tell which one it is. + anyOf: + - $ref: "#/components/schemas/Category" + - $ref: "#/components/schemas/StorageItem" example: - prev_url: "/api/v1/consents?page=" - next_url: "/api/v1/consents?page=2" - count: 68 - page: 1 - prev: null - next: 2 + id: "act123" + action_type: "autofill" + actionable_type: "ClientStorageItem" + actionable_id: "sto123" + suggested_params: + description: "Used to distinguish users." + company_name: "Google" + duration: "2 years" + created_at: "2023-02-01T10:00:00Z" + actionable: + id: "sto123" + name: "_ga" + storage_type: "cookie" - GroupPagination: - allOf: - - $ref: "#/components/schemas/BasePagination" - example: - prev_url: "/api/v1/groups?page=" - next_url: "/api/v1/groups?page=2" - count: 68 - page: 1 - prev: null - next: 2 + AIActionResponse: + type: object + properties: + data: + $ref: "#/components/schemas/AIAction" - StorageItemPagination: - allOf: - - $ref: "#/components/schemas/BasePagination" + Roles: + type: object + description: | + Role flags for a membership. Currently `admin` is the only role, and it + defaults to `true`. + additionalProperties: + type: boolean example: - prev_url: "/api/v1/storage-items?page=" - next_url: "/api/v1/storage-items?page=2" - count: 68 - page: 1 - prev: null - next: 2 + admin: true - LoginRequest: + AccountUserWrite: type: object - properties: - email: - type: string - format: email - description: The user's email address - password: - type: string - format: password - description: The user's password - required: - - email - - password - example: - email: "user@example.com" - password: "correct-horse-battery-staple" + properties: + roles: + $ref: "#/components/schemas/Roles" - LoginResponse: + AccountUserWriteRequest: type: object properties: - token: - type: string - description: API token to send as a bearer token on subsequent requests + account_user: + $ref: "#/components/schemas/AccountUserWrite" + required: + - account_user example: - token: "0mHFuvY2mCVfMBXbtNCPXA" + account_user: + roles: + admin: false - PasswordUpdateRequest: + AccountUser: type: object properties: + id: + type: string + description: Unique identifier for the account user + roles: + $ref: "#/components/schemas/Roles" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time user: type: object properties: - current_password: + id: type: string - format: password - password: + name: type: string - format: password - password_confirmation: + email: + type: string + format: email + account: + type: object + properties: + id: + type: string + name: type: string - format: password - required: - - current_password - - password - - password_confirmation - required: - - user example: + id: "accuser123" + roles: + admin: true + created_at: "2023-01-05T10:15:30Z" + updated_at: "2023-01-15T11:20:45Z" user: - current_password: "correct-horse-battery-staple" - password: "a-longer-new-password" - password_confirmation: "a-longer-new-password" + id: "usr123" + name: "Ada Lovelace" + email: "admin@example.com" + account: + id: "acc123" + name: "example.com" - CategoryWrite: + AccountUserResponse: + type: object + properties: + data: + $ref: "#/components/schemas/AccountUser" + + AccountInvitationCreate: type: object properties: name: type: string - description: Name of the category that is displayed to users - slug: + description: Name of the user the invitation is for + email: type: string - description: | - URL-friendly identifier for the category, used by the consent banner - and the JavaScript SDK. Some slugs are reserved by CookieChimp. - description: + format: email + description: Email address of the user + required: + - name + - email + + AccountInvitationCreateRequest: + type: object + properties: + account_invitation: + $ref: "#/components/schemas/AccountInvitationCreate" + required: + - account_invitation + example: + account_invitation: + name: "Grace Hopper" + email: "newmember@cookiechimp.com" + + AccountInvitationUpdate: + type: object + properties: + name: type: string - description: Description of the category that is displayed to users - required: - type: boolean - description: Whether the category is required for the website to function + description: Name of the user the invitation is for required: - name - CategoryWriteRequest: + AccountInvitationUpdateRequest: type: object properties: - category: - $ref: "#/components/schemas/CategoryWrite" + account_invitation: + $ref: "#/components/schemas/AccountInvitationUpdate" required: - - category + - account_invitation example: - category: - name: "Analytics" - slug: "analytics" - description: "Cookies that help us understand how the site is used" - required: false + account_invitation: + name: "Grace Hopper" - Category: + AccountInvitation: type: object properties: id: type: string - readOnly: true - description: Unique identifier for the category name: type: string - description: Name of the category that is displayed to users - description: - type: string - description: Description of the category that is displayed to users - slug: + description: Name of the user the invitation is for + email: type: string - description: URL-friendly identifier for the category - required: - type: boolean - description: Whether the category is required for the website to function - default_language: + format: email + description: Email address of the user + invite_url: type: string - description: Language code the name and description are written in + description: URL the invited user follows to accept the invitation + roles: + $ref: "#/components/schemas/Roles" created_at: type: string format: date-time - readOnly: true updated_at: type: string format: date-time - readOnly: true + invited_by: + type: [object, "null"] + description: The user who sent the invitation. Empty when it was created via the API. + properties: + id: + type: string + name: + type: string + email: + type: string + format: email example: - id: "essential" - name: "Essential" - description: "Cookies essential for website functionality" - slug: "essential" - required: true - default_language: "en" - created_at: "2023-01-01T00:00:00Z" - updated_at: "2023-01-02T00:00:00Z" + id: "inv123" + name: "Grace Hopper" + email: "newmember@cookiechimp.com" + invite_url: "https://cookiechimp.com/account_invitations/inv123" + roles: + admin: true + created_at: "2023-01-25T09:30:20Z" + updated_at: "2023-01-25T09:30:20Z" + invited_by: + id: "usr123" + name: "Ada Lovelace" + email: "admin@example.com" - CategoryResponse: + AccountInvitationResponse: type: object properties: data: - $ref: "#/components/schemas/Category" + $ref: "#/components/schemas/AccountInvitation" - StorageItemWrite: + VisitorStatsMetric: type: object properties: - name: - type: string - description: Name of the storage item - storage_type: - type: string - enum: [cookie, local_storage, session_storage] - description: The kind of browser storage this item uses - description: - type: string - description: Description of the storage item that is displayed to users - company_name: - type: string - description: Name of the company that owns the storage item - domain: - type: string - description: Domain of the storage item - duration: - type: string - description: How long the storage item persists - partitioned: - type: boolean - description: Whether the storage item is partitioned (CHIPS) - path: - type: string - description: Path of the storage item - samesite: - type: string - enum: [lax, strict] - description: SameSite attribute of the storage item - category_id: - type: string - description: ID of the category to file this item under - site_service_id: - type: string - description: | - ID of the service this item belongs to. When set, the item inherits - the service's category. - required: - - name + count: + type: number + description: Value for the current period + rate_of_change: + type: number + description: Percentage change against the comparison period - StorageItemWriteRequest: + VisitorStatsRawMetric: type: object properties: - storage_item: - $ref: "#/components/schemas/StorageItemWrite" - required: - - storage_item - example: - storage_item: - name: "_ga" - storage_type: "cookie" - description: "Used to distinguish users." - company_name: "Google" - domain: ".example.com" - duration: "2 years" - partitioned: false - path: "/" - samesite: "lax" - category_id: "cat1" - - StorageItem: - type: object + current_period: + type: number + previous_period: + type: number + + VisitorStats: + type: [object, "null"] + description: | + Visitor analytics for the last 30 days, compared against the 30 days + before that. `null` when the client passed `?visitor_stats=false`. properties: - id: - type: string - readOnly: true - description: Unique identifier for the storage item - name: - type: string - storage_type: + comparison: type: string - enum: [cookie, local_storage, session_storage] - category: + description: Label for the comparison period + statistics: type: object - description: | - The category this item is filed under. Falls back to the category of - the item's service when the item has no category of its own. properties: - id: - type: [string, "null"] - name: - type: [string, "null"] - service: + consents: + $ref: "#/components/schemas/VisitorStatsMetric" + unique_visitors: + $ref: "#/components/schemas/VisitorStatsMetric" + consent_rate: + $ref: "#/components/schemas/VisitorStatsMetric" + raw_data: type: object - description: The service this item belongs to, if any properties: - id: - type: [string, "null"] - name: - type: [string, "null"] - description: - type: string - company_name: - type: string - duration: - type: string - domain: + consents: + $ref: "#/components/schemas/VisitorStatsRawMetric" + unique_visitors: + $ref: "#/components/schemas/VisitorStatsRawMetric" + consent_rate: + $ref: "#/components/schemas/VisitorStatsRawMetric" + + Account: + type: object + properties: + id: type: string - partitioned: - type: boolean - path: + readOnly: true + description: Unique identifier for the account + name: type: string - samesite: - type: [string, "null"] - enum: [lax, strict, null] - wildcard_id: - type: [string, "null"] readOnly: true - description: ID of the wildcard item this one was matched against, if any - default_language: + description: Domain of the account + allowed_domains: + type: array + items: + type: string + readOnly: true + description: List of allowed domains the script will run on + owner_id: type: string - description: Language code the name and description are written in - last_seen_at: - type: [string, "null"] - format: date-time readOnly: true - description: When this item was last observed on the website - created_by: + description: ID of the account user that owns the account + onboarding_status: type: string - enum: [user, scan] readOnly: true - description: Whether the item was added by a user or found by a scan + description: | + The status of the account, where: + - `account_created`: The account has been created. + - `vendors_setup`: Categories, services or TCF vendors have been set up. + - `code_installed`: The script has been installed on the user's website. + - `inactive`: Less than 10 visitors to the user's website in the last 30 days. + - `live`: The account is live and operational. + visitor_stats: + $ref: "#/components/schemas/VisitorStats" created_at: type: string format: date-time readOnly: true + description: Date and time when the account was created updated_at: type: string format: date-time readOnly: true + description: Date and time when the account was last updated example: - id: "sto123" - name: "_ga" - storage_type: "cookie" - category: - id: "cat1" - name: "Analytics" - service: - id: "svc1" - name: "Google Analytics" - description: "Used to distinguish users." - company_name: "Google" - duration: "2 years" - domain: ".example.com" - partitioned: false - path: "/" - samesite: "lax" - wildcard_id: null - default_language: "en" - last_seen_at: "2023-03-23T12:34:56Z" - created_by: "scan" - created_at: "2023-03-01T09:00:00Z" - updated_at: "2023-03-23T12:34:56Z" - - StorageItemResponse: - type: object - properties: - data: - $ref: "#/components/schemas/StorageItem" + id: "abc123" + name: "example.com" + allowed_domains: ["example.net", "example.org"] + owner_id: "xyz123" + onboarding_status: "live" + visitor_stats: + comparison: "vs. Last 30 days" + statistics: + consents: + count: 213 + rate_of_change: 25.29 + unique_visitors: + count: 895 + rate_of_change: 42.06 + consent_rate: + count: 23.8 + rate_of_change: -11.85 + raw_data: + consents: + current_period: 213 + previous_period: 170 + unique_visitors: + current_period: 895 + previous_period: 630 + consent_rate: + current_period: 23.8 + previous_period: 27.0 + created_at: "2022-12-20T18:45:00Z" + updated_at: "2023-01-10T19:00:00Z" - GroupCreate: + AccountWrite: type: object properties: name: type: string - description: Name of the group - external_id: + description: | + The account's domain. Must be a valid domain for website accounts. + domain: type: string - description: External ID set by the partner - max_pageviews_per_month: - type: integer - description: Maximum number of pageviews per month - max_unique_visitors_per_month: - type: integer - description: Maximum number of unique visitors per month - required: - - name - - external_id - - GroupCreateRequest: - type: object - properties: - group: - $ref: "#/components/schemas/GroupCreate" - required: - - group - example: - group: - name: "Example Ltd" - external_id: "ABC_123" - max_pageviews_per_month: 500000 - max_unique_visitors_per_month: 100000 - - GroupUpdate: - type: object - properties: - name: + subdomain: type: string - description: Name of the group - external_id: + default_language: type: string - description: External ID set by the partner - required: - - name - - external_id - - GroupUpdateRequest: - type: object - properties: - group: - $ref: "#/components/schemas/GroupUpdate" - required: - - group - example: - group: - name: "Example Ltd" - external_id: "ABC_123" - - Group: - type: object - properties: - id: + description: ISO 639-1 code the banner falls back to + auto_translate: + type: boolean + description: Whether new content is machine-translated into the account's languages + translations: + type: array + items: + type: string + description: ISO 639-1 codes the banner is translated into + allowed_domains_list: type: string - readOnly: true - description: Unique identifier for the group - partner: + description: Comma-separated list of domains the script may run on + industry: type: string - readOnly: true - description: Name of the partner the group belongs to - name: + description: type: string - description: Name of the group - external_id: + support_email: type: string - description: External ID set by the partner - max_pageviews_per_month: - type: [integer, "null"] - description: Maximum number of pageviews per month - max_unique_visitors_per_month: - type: [integer, "null"] - description: Maximum number of unique visitors per month - example: - id: "abc123" - partner: "partner_one" - name: "Example Ltd" - external_id: "ABC_123" - max_pageviews_per_month: 500000 - max_unique_visitors_per_month: 100000 + format: email + privacy_policy_url: + type: string + banner_debug_mode: + type: boolean + blocking_mode: + type: string + description: How aggressively scripts are blocked before consent + retain_consent_duration: + type: integer + description: | + Years consent records are retained. Free accounts are fixed at 3. - Consent: + AccountWriteRequest: + type: object + properties: + account: + $ref: "#/components/schemas/AccountWrite" + required: + - account + example: + account: + name: "example.com" + support_email: "privacy@example.com" + banner_debug_mode: false + + SiteServiceWrite: type: object - description: | - A consent record, written when a visitor interacts with the consent - banner. properties: - id: - type: string - description: Unique identifier for the consent - consent_banner_id: - type: [string, "null"] - description: ID of the consent banner the visitor saw - uuid: + name: type: string - description: Anonymous visitor identifier generated by the widget - url: + description: Name of the service, shown to visitors + category_id: type: string - description: URL the consent was given on - ip_address: - type: [string, "null"] - description: Partially masked IP address of the visitor - ip_country: - type: [string, "null"] - description: Two-letter country code resolved from the visitor's IP - ip_region: - type: [string, "null"] - description: Region resolved from the visitor's IP - visitor_user_id: - type: [string, "null"] - description: ID of the user set by your website - accept_type: - type: [string, "null"] description: | - How the visitor responded. Typically `all`, `custom` or `necessary`. - accepted_categories: - type: [array, "null"] - items: - type: string - description: Slugs of the categories the visitor accepted - rejected_categories: - type: [array, "null"] - items: - type: string - description: Slugs of the categories the visitor rejected - accepted_services: - type: [object, "null"] - additionalProperties: - type: array - items: - type: string - description: Accepted services, keyed by category slug - rejected_services: - type: [object, "null"] - additionalProperties: - type: array - items: - type: string - description: Rejected services, keyed by category slug - accepted_cookies: - type: [array, "null"] - items: - type: object - description: Storage items the visitor accepted - rejected_cookies: - type: [array, "null"] - items: - type: object - description: Storage items the visitor rejected - device: + ID of the category the service belongs to. Required on create; + on update, supplying it moves the service to that category. + description: type: string - description: Device type derived from the user agent - user_agent: + description: Description shown to visitors + processing_company: type: string - description: User agent of the visitor's browser - gpc_preference: - type: [boolean, "null"] - description: Whether the visitor sent a Global Privacy Control signal - do_not_sell: + description: Company that processes data through this service + privacy_policy_url: + type: string + additional_info: + type: string + accepted_by_default: type: boolean - description: Whether the visitor opted out of the sale of their data - created_at: + description: Whether the service is pre-accepted in the banner + enabled: + type: boolean + description: Whether the service is active on the account + auto_block: + type: boolean + description: Whether the widget blocks this service's scripts before consent + auto_block_domains_input: type: string - format: date-time - description: Date and time when the consent was recorded - example: - id: "consent321" - consent_banner_id: "banner123" - uuid: "8f14e45f-ea8f-4f2e-b1b0-0f1d2c3b4a59" - url: "https://example.com/pricing" - ip_address: "203.0.113.XXX" - ip_country: "GB" - ip_region: "England" - visitor_user_id: "user456" - accept_type: "custom" - accepted_categories: ["essential", "analytics"] - rejected_categories: ["marketing"] - accepted_services: { "analytics": ["Google Analytics"] } - rejected_services: { "marketing": ["Meta Pixel"] } - accepted_cookies: [] - rejected_cookies: [] - device: "desktop" - user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" - gpc_preference: false - do_not_sell: false - created_at: "2023-01-20T12:46:30Z" + description: | + Domains to auto-block, separated by commas or newlines. Parsed into + the `auto_block_domains` list on save. + required: + - name - ConsentResponse: + SiteServiceWriteRequest: type: object properties: - data: - $ref: "#/components/schemas/Consent" + site_service: + $ref: "#/components/schemas/SiteServiceWrite" + required: + - site_service + example: + site_service: + name: "Google Analytics" + category_id: "cat1" + processing_company: "Google LLC" + privacy_policy_url: "https://policies.google.com/privacy" + auto_block: true - AIAction: + SiteService: type: object - description: An action CookieChimp's AI took on a category or storage item. properties: id: type: string - description: Unique identifier for the AI action - action_type: - type: string - enum: [autofill, auto_categorise, translate] - description: What the AI did - actionable_type: - type: string - enum: [Category, ClientStorageItem] - description: The type of record the action was taken on - actionable_id: + readOnly: true + name: type: string - description: ID of the record the action was taken on - suggested_params: - type: [object, "null"] - additionalProperties: true - description: The values the AI suggested + description: + type: [string, "null"] + processing_company: + type: [string, "null"] + privacy_policy_url: + type: [string, "null"] + additional_info: + type: [string, "null"] + accepted_by_default: + type: boolean + enabled: + type: boolean + auto_block: + type: boolean + auto_block_domains: + type: array + items: + type: string + description: Domains the widget blocks for this service before consent + default_language: + type: [string, "null"] + category: + type: object + description: The category this service is filed under + properties: + id: + type: [string, "null"] + name: + type: [string, "null"] created_at: type: string format: date-time - actionable: - description: | - The record the action was taken on — a category or a storage item, - in the same shape those endpoints return. Use `actionable_type` to - tell which one it is. - anyOf: - - $ref: "#/components/schemas/Category" - - $ref: "#/components/schemas/StorageItem" + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true example: - id: "act123" - action_type: "autofill" - actionable_type: "ClientStorageItem" - actionable_id: "sto123" - suggested_params: - description: "Used to distinguish users." - company_name: "Google" - duration: "2 years" - created_at: "2023-02-01T10:00:00Z" - actionable: - id: "sto123" - name: "_ga" - storage_type: "cookie" + id: "svc1" + name: "Google Analytics" + description: "Measures how visitors use the site." + processing_company: "Google LLC" + privacy_policy_url: "https://policies.google.com/privacy" + additional_info: null + accepted_by_default: false + enabled: true + auto_block: true + auto_block_domains: ["google-analytics.com"] + default_language: "en" + category: + id: "cat1" + name: "Analytics" + created_at: "2023-03-01T09:00:00Z" + updated_at: "2023-03-23T12:34:56Z" - AIActionResponse: + SiteServiceResponse: type: object properties: data: - $ref: "#/components/schemas/AIAction" + $ref: "#/components/schemas/SiteService" - Roles: + ConsentBannerWrite: type: object description: | - Role flags for a membership. Currently `admin` is the only role, and it - defaults to `true`. - additionalProperties: - type: boolean - example: - admin: true - - AccountUserWrite: - type: object + The banner attributes the dashboard's banner designer can set. The full + set is large; the commonly used ones are listed here, and any other + attribute the designer exposes is accepted too. properties: - roles: - $ref: "#/components/schemas/Roles" + name: + type: string + enabled: + type: boolean + description: Whether the banner is live. New banners are always created disabled. + enable_tcf: + type: boolean + description: Whether the banner runs in IAB TCF mode + force_consent: + type: boolean + description: Whether the page is blocked until the visitor chooses + consent_mode: + type: string + description: e.g. `opt_in`, `opt_out` + consent_duration_days: + type: integer + description: How long a visitor's choice is remembered + consent_layout: + type: string + description: e.g. `box`, `bar` + consent_position: + type: string + description: e.g. `bottom_center` + consent_title: + type: string + consent_description: + type: string + privacy_policy_url: + type: string + preferences_control_level: + type: string + description: How granular the preferences modal is + dark_mode: + type: string + description: e.g. `light`, `dark`, `auto` + language_autodetect: + type: string + target_type: + type: string + description: Who sees the banner, e.g. `worldwide`, `selected_countries` + target_countries: + type: array + items: + type: string + description: ISO 3166-1 alpha-2 codes + target_us_states: + type: array + items: + type: string - AccountUserWriteRequest: + ConsentBannerWriteRequest: type: object properties: - account_user: - $ref: "#/components/schemas/AccountUserWrite" + consent_banner: + $ref: "#/components/schemas/ConsentBannerWrite" required: - - account_user + - consent_banner example: - account_user: - roles: - admin: false - - AccountUser: + consent_banner: + name: "EU banner" + consent_title: "We use cookies" + consent_mode: "opt_in" + target_type: "selected_countries" + target_countries: ["DE", "FR"] + + ConsentBanner: type: object properties: id: type: string - description: Unique identifier for the account user - roles: - $ref: "#/components/schemas/Roles" + readOnly: true + name: + type: string + enabled: + type: boolean + enable_tcf: + type: boolean + force_consent: + type: boolean + consent_mode: + type: string + consent_duration_days: + type: integer + consent_layout: + type: string + consent_position: + type: string + consent_title: + type: [string, "null"] + consent_description: + type: [string, "null"] + privacy_policy_url: + type: [string, "null"] + preferences_control_level: + type: string + dark_mode: + type: string + language_autodetect: + type: string + revision: + type: integer + readOnly: true + description: Bumped when a change requires visitors to consent again + targeting: + type: object + properties: + target_type: + type: string + target_countries: + type: array + items: + type: string + target_us_states: + type: array + items: + type: string + languages: + type: array + items: + type: string + readOnly: true + description: ISO 639-1 codes this banner has translations for created_at: type: string format: date-time + readOnly: true updated_at: type: string format: date-time - user: - type: object - properties: - id: - type: string - name: - type: string - email: - type: string - format: email - account: - type: object - properties: - id: - type: string - name: - type: string + readOnly: true example: - id: "accuser123" - roles: - admin: true + id: "ban1" + name: "EU banner" + enabled: true + enable_tcf: false + force_consent: false + consent_mode: "opt_in" + consent_duration_days: 180 + consent_layout: "box" + consent_position: "bottom_center" + consent_title: "We use cookies" + consent_description: "We use cookies to improve your experience." + privacy_policy_url: "https://example.com/privacy" + preferences_control_level: "category_and_services" + dark_mode: "light" + language_autodetect: "document" + revision: 2 + targeting: + target_type: "selected_countries" + target_countries: ["DE", "FR"] + target_us_states: [] + languages: ["en", "de", "fr"] created_at: "2023-01-05T10:15:30Z" - updated_at: "2023-01-15T11:20:45Z" - user: - id: "usr123" - name: "Ada Lovelace" - email: "admin@example.com" - account: - id: "acc123" - name: "example.com" + updated_at: "2023-02-01T08:00:00Z" - AccountUserResponse: + ConsentBannerResponse: type: object properties: data: - $ref: "#/components/schemas/AccountUser" + $ref: "#/components/schemas/ConsentBanner" - AccountInvitationCreate: + IntegrationWrite: type: object + description: | + `status` applies to every integration. The remaining keys are only + accepted by the integration that declares them; anything else is ignored. properties: - name: + status: type: string - description: Name of the user the invitation is for - email: + enum: [active, inactive] + scan_frequency: type: string - format: email - description: Email address of the user - required: - - name - - email + enum: [weekly, monthly] + description: | + `SiteScanIntegration` only. Weekly is not available on free accounts. + auto_add_vendors: + type: boolean + description: "`SiteScanIntegration` only: add vendors a scan discovers." + url_passthrough: + type: boolean + description: "`GoogleConsentModeIntegration` only." + ads_data_redaction: + type: boolean + description: "`GoogleConsentModeIntegration` only." - AccountInvitationCreateRequest: + IntegrationWriteRequest: type: object properties: - account_invitation: - $ref: "#/components/schemas/AccountInvitationCreate" + integration: + $ref: "#/components/schemas/IntegrationWrite" required: - - account_invitation + - integration example: - account_invitation: - name: "Grace Hopper" - email: "newmember@cookiechimp.com" + integration: + status: "active" + scan_frequency: "monthly" + auto_add_vendors: true - AccountInvitationUpdate: + Integration: type: object properties: + id: + type: string + readOnly: true + type: + type: string + readOnly: true + description: The integration class, e.g. `SiteScanIntegration` name: type: string - description: Name of the user the invitation is for - required: - - name + readOnly: true + description: Human-readable name + status: + type: string + enum: [active, inactive] + short_description: + type: [string, "null"] + readOnly: true + settings: + type: [object, "null"] + additionalProperties: true + description: Integration-specific settings + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true + example: + id: "int1" + type: "SiteScanIntegration" + name: "Site Scan" + status: "active" + short_description: "Scan your website for cookies on a schedule." + settings: + scan_frequency: "monthly" + auto_add_vendors: false + created_at: "2023-01-05T10:15:30Z" + updated_at: "2023-02-01T08:00:00Z" - AccountInvitationUpdateRequest: + IntegrationResponse: type: object properties: - account_invitation: - $ref: "#/components/schemas/AccountInvitationUpdate" + data: + $ref: "#/components/schemas/Integration" + + ScanWrite: + type: object + properties: + starting_url: + type: string + description: | + URL the crawl starts from. A missing scheme is treated as `https://`. + Defaults to the account's own domain when omitted. + max_pages: + type: integer + minimum: 1 + maximum: 100 + description: Maximum pages to crawl + crawl_subdomains: + type: boolean + description: Whether the crawl follows links onto subdomains + + ScanWriteRequest: + type: object + properties: + scan: + $ref: "#/components/schemas/ScanWrite" required: - - account_invitation + - scan example: - account_invitation: - name: "Grace Hopper" + scan: + starting_url: "https://example.com" + max_pages: 25 + crawl_subdomains: false - AccountInvitation: + Scan: type: object properties: id: type: string - name: + readOnly: true + status: type: string - description: Name of the user the invitation is for - email: + enum: [queued, in_progress, completed, failed] + readOnly: true + starting_url: type: string - format: email - description: Email address of the user - invite_url: + domain: type: string - description: URL the invited user follows to accept the invitation - roles: - $ref: "#/components/schemas/Roles" + readOnly: true + description: Host derived from `starting_url`, without a `www.` prefix + max_pages: + type: [integer, "null"] + crawl_subdomains: + type: boolean + response_code: + type: [string, "null"] + readOnly: true + report: + type: object + readOnly: true + description: | + Only present once `status` is `completed`. Storage items found + before and after consent, grouped by vendor and storage type. + properties: + before_consent: + type: object + additionalProperties: true + after_consent: + type: object + additionalProperties: true created_at: type: string format: date-time + readOnly: true updated_at: type: string format: date-time - invited_by: - type: [object, "null"] - description: The user who sent the invitation. Empty when it was created via the API. - properties: - id: - type: string - name: - type: string - email: - type: string - format: email + readOnly: true example: - id: "inv123" - name: "Grace Hopper" - email: "newmember@cookiechimp.com" - invite_url: "https://cookiechimp.com/account_invitations/inv123" - roles: - admin: true - created_at: "2023-01-25T09:30:20Z" - updated_at: "2023-01-25T09:30:20Z" - invited_by: - id: "usr123" - name: "Ada Lovelace" - email: "admin@example.com" + id: "scn1" + status: "queued" + starting_url: "https://example.com" + domain: "example.com" + max_pages: 25 + crawl_subdomains: false + response_code: null + created_at: "2023-03-01T09:00:00Z" + updated_at: "2023-03-01T09:00:00Z" - AccountInvitationResponse: + ScanResponse: type: object properties: data: - $ref: "#/components/schemas/AccountInvitation" + $ref: "#/components/schemas/Scan" - VisitorStatsMetric: + TcfVendorWrite: type: object properties: - count: - type: number - description: Value for the current period - rate_of_change: - type: number - description: Percentage change against the comparison period + global_vendor_id: + type: integer + description: ID of the vendor in the IAB Global Vendor List + required: + - global_vendor_id - VisitorStatsRawMetric: + TcfVendorWriteRequest: type: object properties: - current_period: - type: number - previous_period: - type: number - - VisitorStats: - type: [object, "null"] - description: | - Visitor analytics for the last 30 days, compared against the 30 days - before that. `null` when the client passed `?visitor_stats=false`. - properties: - comparison: - type: string - description: Label for the comparison period - statistics: - type: object - properties: - consents: - $ref: "#/components/schemas/VisitorStatsMetric" - unique_visitors: - $ref: "#/components/schemas/VisitorStatsMetric" - consent_rate: - $ref: "#/components/schemas/VisitorStatsMetric" - raw_data: - type: object - properties: - consents: - $ref: "#/components/schemas/VisitorStatsRawMetric" - unique_visitors: - $ref: "#/components/schemas/VisitorStatsRawMetric" - consent_rate: - $ref: "#/components/schemas/VisitorStatsRawMetric" + tcf_vendor: + $ref: "#/components/schemas/TcfVendorWrite" + required: + - tcf_vendor + example: + tcf_vendor: + global_vendor_id: 755 - Account: + TcfVendor: type: object properties: id: type: string readOnly: true - description: Unique identifier for the account - name: - type: string - readOnly: true - description: Domain of the account - allowed_domains: - type: array - items: - type: string - readOnly: true - description: List of allowed domains the script will run on - owner_id: + global_vendor_id: + type: integer + status: type: string + enum: [active, removed] readOnly: true - description: ID of the account user that owns the account - onboarding_status: - type: string + vendor: + type: object readOnly: true description: | - The status of the account, where: - - `account_created`: The account has been created. - - `vendors_setup`: Categories, services or TCF vendors have been set up. - - `code_installed`: The script has been installed on the user's website. - - `inactive`: Less than 10 visitors to the user's website in the last 30 days. - - `live`: The account is live and operational. - visitor_stats: - $ref: "#/components/schemas/VisitorStats" + Details from the IAB Global Vendor List. Empty when the ID has been + retired upstream, since the list is not stored per account. + properties: + name: + type: [string, "null"] + urls: + type: [array, "null"] + items: + type: object + additionalProperties: true created_at: type: string format: date-time readOnly: true - description: Date and time when the account was created updated_at: type: string format: date-time readOnly: true - description: Date and time when the account was last updated example: - id: "abc123" - name: "example.com" - allowed_domains: ["example.net", "example.org"] - owner_id: "xyz123" - onboarding_status: "live" - visitor_stats: - comparison: "vs. Last 30 days" - statistics: - consents: - count: 213 - rate_of_change: 25.29 - unique_visitors: - count: 895 - rate_of_change: 42.06 - consent_rate: - count: 23.8 - rate_of_change: -11.85 - raw_data: - consents: - current_period: 213 - previous_period: 170 - unique_visitors: - current_period: 895 - previous_period: 630 - consent_rate: - current_period: 23.8 - previous_period: 27.0 - created_at: "2022-12-20T18:45:00Z" - updated_at: "2023-01-10T19:00:00Z" + id: "tcf1" + global_vendor_id: 755 + status: "active" + vendor: + name: "Google Advertising Products" + urls: + - langId: "en" + privacy: "https://policies.google.com/privacy" + created_at: "2023-01-05T10:15:30Z" + updated_at: "2023-01-05T10:15:30Z" + + TcfVendorResponse: + type: object + properties: + data: + $ref: "#/components/schemas/TcfVendor" security: - authorization: [] From 0e7b3277bdee5b0eba062043ea4c19ac598db876 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 13:33:37 +0000 Subject: [PATCH 02/19] Require category_id when creating a service, not when updating one POST and PUT shared one schema, and it required only name. The prose said category_id was required on create, but a generated client would have accepted {"site_service": {"name": "Example"}} and got a 422 back from the API. Split the two: create requires name and category_id, update requires neither, since the controller permits a partial update and supplying category_id there is what moves the service to another category. Checked against a JSON Schema validator: a create with only a name is now rejected, a create with both is accepted, and a partial update still is. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- openapi.yaml | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index c912e77..ba9495e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1064,7 +1064,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/SiteServiceWriteRequest" + $ref: "#/components/schemas/SiteServiceCreateRequest" responses: "201": description: Created service @@ -1124,7 +1124,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/SiteServiceWriteRequest" + $ref: "#/components/schemas/SiteServiceUpdateRequest" responses: "200": description: Updated service @@ -2934,14 +2934,22 @@ components: description: | Domains to auto-block, separated by commas or newlines. Parsed into the `auto_block_domains` list on save. + + SiteServiceCreate: + description: | + A service is always filed under a category, so `category_id` is + required here even though it is optional when updating. + allOf: + - $ref: "#/components/schemas/SiteServiceWrite" required: - name + - category_id - SiteServiceWriteRequest: + SiteServiceCreateRequest: type: object properties: site_service: - $ref: "#/components/schemas/SiteServiceWrite" + $ref: "#/components/schemas/SiteServiceCreate" required: - site_service example: @@ -2952,6 +2960,21 @@ components: privacy_policy_url: "https://policies.google.com/privacy" auto_block: true + SiteServiceUpdateRequest: + type: object + description: | + Every field is optional; anything omitted keeps its current value. + Supplying `category_id` moves the service to that category. + properties: + site_service: + $ref: "#/components/schemas/SiteServiceWrite" + required: + - site_service + example: + site_service: + name: "Google Analytics 4" + auto_block: false + SiteService: type: object properties: From 1b5a8e7dca7f0863c67aefe7b65d7dd36fc0f1d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 13:51:51 +0000 Subject: [PATCH 03/19] Document admin-only mutations and the review fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the code changes in CookieChimp#2554 made in response to review. Configuration mutations are admin-only, so the eleven mutating operations now declare a 403 alongside account update, and AccountAdminRequired says what earns it: an account key carries the account's own authority, a user key needs an admin role, and a group role supersedes a direct membership. Reads are unaffected. Contract changes: - A banner's `languages` array of codes becomes `translations`, each with an `id`. The id is what you send back in `banner_languages_attributes`, and without one a client cannot update a translation it created. - `privacy_policy_url` is gone from the account schema. It is an attr_accessor consumed when default banners are built at creation, so update reported success and stored nothing. - Banner creation no longer accepts translations; they are added by updating the banner, which is where the language entitlement is enforced. - The integrations listing is platform-aware — a mobile app account does not see website-only integrations. - Scanning is website-only, so a mobile app account gets a 422. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- openapi.yaml | 60 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index ba9495e..5aeb51f 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1074,6 +1074,8 @@ paths: $ref: "#/components/schemas/SiteServiceResponse" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "404": description: The referenced `category_id` does not belong to this account content: @@ -1134,6 +1136,8 @@ paths: $ref: "#/components/schemas/SiteServiceResponse" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "404": $ref: "#/components/responses/NotFound" "422": @@ -1160,6 +1164,8 @@ paths: message: "Service was deleted successfully" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "404": $ref: "#/components/responses/NotFound" "429": @@ -1206,6 +1212,10 @@ paths: recently updated banner, the same way the dashboard's new-banner flow does, and is always created disabled — enable it once its content and targeting are set. A name is generated if you don't supply one. + + Translations are not accepted here; add them by updating the banner + afterwards, which is where the account's language entitlement is + enforced. operationId: createConsentBanner tags: - Consent Banners @@ -1225,6 +1235,8 @@ paths: $ref: "#/components/schemas/ConsentBannerResponse" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "422": $ref: "#/components/responses/ValidationError" "429": @@ -1279,6 +1291,8 @@ paths: $ref: "#/components/schemas/ConsentBannerResponse" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "404": $ref: "#/components/responses/NotFound" "422": @@ -1305,6 +1319,8 @@ paths: message: "Consent banner was deleted successfully" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "404": $ref: "#/components/responses/NotFound" "429": @@ -1330,6 +1346,8 @@ paths: $ref: "#/components/schemas/ConsentBannerResponse" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "404": $ref: "#/components/responses/NotFound" "422": @@ -1345,6 +1363,9 @@ paths: List every integration available to the account, active and inactive. Integrations the account has never opened are created inactive on first listing, so the collection is always the full set you can enable. + + Only integrations the account's platform can run are included — a mobile + app account does not see the website-only ones. operationId: listIntegrations tags: - Integrations @@ -1430,6 +1451,8 @@ paths: $ref: "#/components/schemas/IntegrationResponse" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "404": $ref: "#/components/responses/NotFound" "422": @@ -1476,7 +1499,7 @@ paths: description: | Queue a scan of the account's website. Only one scan can run at a time per account, and accounts have a daily scan limit — both come back as a - `422`. + `422`, as does a mobile app account, which has no website to crawl. Scanning is asynchronous: the scan is created `queued`, and the report appears on the scan once its status reaches `completed`. @@ -1499,6 +1522,8 @@ paths: $ref: "#/components/schemas/ScanResponse" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "422": $ref: "#/components/responses/ValidationError" "429": @@ -1591,6 +1616,8 @@ paths: $ref: "#/components/schemas/TcfVendorResponse" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "422": $ref: "#/components/responses/ValidationError" "429": @@ -1640,6 +1667,8 @@ paths: message: "TCF vendor was deleted successfully" "401": $ref: "#/components/responses/AccountTokenRequired" + "403": + $ref: "#/components/responses/AccountAdminRequired" "404": $ref: "#/components/responses/NotFound" "429": @@ -1758,7 +1787,10 @@ components: AccountAdminRequired: description: | The token resolved to a user who is not an admin of this account. - Account tokens carry the account's own authority and never see this. + Configuration changes are admin-only, mirroring the dashboard — a group + admin qualifies even without a direct membership, since a group role + supersedes one. Account tokens carry the account's own authority and + never see this; reads are unaffected. content: application/json: schema: @@ -2874,8 +2906,6 @@ components: support_email: type: string format: email - privacy_policy_url: - type: string banner_debug_mode: type: boolean blocking_mode: @@ -3167,12 +3197,20 @@ components: type: array items: type: string - languages: + translations: type: array - items: - type: string readOnly: true - description: ISO 639-1 codes this banner has translations for + description: | + The banner's translations. The `id` is what you send back in + `banner_languages_attributes` to update or discard one. + items: + type: object + properties: + id: + type: string + language_code: + type: string + description: ISO 639-1 code created_at: type: string format: date-time @@ -3202,7 +3240,11 @@ components: target_type: "selected_countries" target_countries: ["DE", "FR"] target_us_states: [] - languages: ["en", "de", "fr"] + translations: + - id: "trn1" + language_code: "de" + - id: "trn2" + language_code: "fr" created_at: "2023-01-05T10:15:30Z" updated_at: "2023-02-01T08:00:00Z" From ba1cc9931b3cea349c07d3705120c84f80b05aae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:11:31 +0000 Subject: [PATCH 04/19] Drop TCF vendors, document compliance report and banner templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows CookieChimp#2554. TCF vendor management comes out of the API for now, so its four operations, schemas, reference pages and nav group are removed. Adds three operations: - GET /compliance — the regional compliance report, with per-regulation issues, warnings and satisfied requirements. Ignored regulations are reported separately and excluded from the top-level verdict. - GET /banner-templates — jurisdiction templates grouped by region. - POST /consent-banners/from-template — a banner preconfigured for a jurisdiction. Also a new 400. A user API key that can reach more than one account must name the account it means when writing, since the alternative is a write landing on a silently chosen one; AmbiguousAccount documents that and is declared on the nine write operations it can affect. 53 operations, each with a reference page and a nav entry. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- .../compliance/get-compliance-report.mdx | 3 + .../create-consent-banner-from-template.mdx | 3 + .../consent-banners/list-banner-templates.mdx | 3 + .../tcf-vendors/create-tcf-vendor.mdx | 3 - .../tcf-vendors/delete-tcf-vendor.mdx | 3 - api-reference/tcf-vendors/get-tcf-vendor.mdx | 3 - .../tcf-vendors/list-tcf-vendors.mdx | 3 - docs.json | 18 +- openapi.yaml | 341 +++++++++++------- 9 files changed, 229 insertions(+), 151 deletions(-) create mode 100644 api-reference/compliance/get-compliance-report.mdx create mode 100644 api-reference/consent-banners/create-consent-banner-from-template.mdx create mode 100644 api-reference/consent-banners/list-banner-templates.mdx delete mode 100644 api-reference/tcf-vendors/create-tcf-vendor.mdx delete mode 100644 api-reference/tcf-vendors/delete-tcf-vendor.mdx delete mode 100644 api-reference/tcf-vendors/get-tcf-vendor.mdx delete mode 100644 api-reference/tcf-vendors/list-tcf-vendors.mdx diff --git a/api-reference/compliance/get-compliance-report.mdx b/api-reference/compliance/get-compliance-report.mdx new file mode 100644 index 0000000..3cdf05e --- /dev/null +++ b/api-reference/compliance/get-compliance-report.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /compliance +--- diff --git a/api-reference/consent-banners/create-consent-banner-from-template.mdx b/api-reference/consent-banners/create-consent-banner-from-template.mdx new file mode 100644 index 0000000..3e0ef73 --- /dev/null +++ b/api-reference/consent-banners/create-consent-banner-from-template.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /consent-banners/from-template +--- diff --git a/api-reference/consent-banners/list-banner-templates.mdx b/api-reference/consent-banners/list-banner-templates.mdx new file mode 100644 index 0000000..160f090 --- /dev/null +++ b/api-reference/consent-banners/list-banner-templates.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /banner-templates +--- diff --git a/api-reference/tcf-vendors/create-tcf-vendor.mdx b/api-reference/tcf-vendors/create-tcf-vendor.mdx deleted file mode 100644 index fe0a612..0000000 --- a/api-reference/tcf-vendors/create-tcf-vendor.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: post /tcf-vendors ---- diff --git a/api-reference/tcf-vendors/delete-tcf-vendor.mdx b/api-reference/tcf-vendors/delete-tcf-vendor.mdx deleted file mode 100644 index d2b5bc0..0000000 --- a/api-reference/tcf-vendors/delete-tcf-vendor.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: delete /tcf-vendors/{id} ---- diff --git a/api-reference/tcf-vendors/get-tcf-vendor.mdx b/api-reference/tcf-vendors/get-tcf-vendor.mdx deleted file mode 100644 index fed7990..0000000 --- a/api-reference/tcf-vendors/get-tcf-vendor.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: get /tcf-vendors/{id} ---- diff --git a/api-reference/tcf-vendors/list-tcf-vendors.mdx b/api-reference/tcf-vendors/list-tcf-vendors.mdx deleted file mode 100644 index 56d1dec..0000000 --- a/api-reference/tcf-vendors/list-tcf-vendors.mdx +++ /dev/null @@ -1,3 +0,0 @@ ---- -openapi: get /tcf-vendors ---- diff --git a/docs.json b/docs.json index 2e3296e..5a94bff 100644 --- a/docs.json +++ b/docs.json @@ -14,7 +14,12 @@ "library": "lucide" }, "contextual": { - "options": ["copy", "view", "chatgpt", "claude"] + "options": [ + "copy", + "view", + "chatgpt", + "claude" + ] }, "favicon": "/favicon.png", "navigation": { @@ -172,6 +177,8 @@ "pages": [ "api-reference/consent-banners/list-consent-banners", "api-reference/consent-banners/create-consent-banner", + "api-reference/consent-banners/list-banner-templates", + "api-reference/consent-banners/create-consent-banner-from-template", "api-reference/consent-banners/get-consent-banner", "api-reference/consent-banners/update-consent-banner", "api-reference/consent-banners/duplicate-consent-banner", @@ -189,12 +196,9 @@ ] }, { - "group": "TCF Vendors", + "group": "Compliance", "pages": [ - "api-reference/tcf-vendors/list-tcf-vendors", - "api-reference/tcf-vendors/create-tcf-vendor", - "api-reference/tcf-vendors/get-tcf-vendor", - "api-reference/tcf-vendors/delete-tcf-vendor" + "api-reference/compliance/get-compliance-report" ] }, { @@ -364,4 +368,4 @@ "appId": "r4jecam1" } } -} \ No newline at end of file +} diff --git a/openapi.yaml b/openapi.yaml index 5aeb51f..7418bf5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1072,6 +1072,8 @@ paths: application/json: schema: $ref: "#/components/schemas/SiteServiceResponse" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1134,6 +1136,8 @@ paths: application/json: schema: $ref: "#/components/schemas/SiteServiceResponse" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1162,6 +1166,8 @@ paths: $ref: "#/components/schemas/DeletedMessage" example: message: "Service was deleted successfully" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1233,6 +1239,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ConsentBannerResponse" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1289,6 +1297,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ConsentBannerResponse" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1317,6 +1327,8 @@ paths: $ref: "#/components/schemas/DeletedMessage" example: message: "Consent banner was deleted successfully" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1344,6 +1356,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ConsentBannerResponse" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1449,6 +1463,8 @@ paths: application/json: schema: $ref: "#/components/schemas/IntegrationResponse" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1520,6 +1536,8 @@ paths: application/json: schema: $ref: "#/components/schemas/ScanResponse" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": @@ -1554,123 +1572,108 @@ paths: "429": $ref: "#/components/responses/RateLimited" - # TCF vendor endpoints - index, GET, POST & DELETE - /tcf-vendors: + # compliance report + /compliance: get: - summary: List TCF vendors + summary: Get the regional compliance report description: | - List the IAB TCF vendors enabled on the account, in global vendor ID - order. Requires an account-scoped API token. - operationId: listTcfVendors + Evaluate the account's banners against every regulation CookieChimp + tracks, and report what is outstanding — the same evaluation the + dashboard's compliance screen renders. + + Regulations the account has chosen to ignore are reported separately in + `ignored_jurisdictions` rather than dropped, and are excluded from the + top-level `compliant` verdict. + + Requires a plan that includes the compliance report. + operationId: getComplianceReport tags: - - TCF Vendors - parameters: - - $ref: "#/components/parameters/per_page" - - $ref: "#/components/parameters/page" + - Compliance responses: "200": - description: List of TCF vendors + description: The compliance report content: application/json: schema: - type: object - properties: - pagination: - $ref: "#/components/schemas/BasePagination" - data: - type: array - items: - $ref: "#/components/schemas/TcfVendor" - "400": - $ref: "#/components/responses/BadRequest" + $ref: "#/components/schemas/ComplianceReportResponse" "401": $ref: "#/components/responses/AccountTokenRequired" - "429": - $ref: "#/components/responses/RateLimited" - - post: - summary: Add a TCF vendor - description: | - Add a vendor from the IAB Global Vendor List to the account, one at a - time, so you can reconcile against your own list without restating it. - - Re-adding a vendor that was previously removed restores the original - record rather than creating a second one. A vendor already on the - account, or an ID that isn't in the Global Vendor List, returns a `422`. - operationId: createTcfVendor - tags: - - TCF Vendors - requestBody: - description: Vendor to add - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/TcfVendorWriteRequest" - responses: - "201": - description: The added vendor + "403": + description: The account's plan does not include the compliance report content: application/json: schema: - $ref: "#/components/schemas/TcfVendorResponse" - "401": - $ref: "#/components/responses/AccountTokenRequired" - "403": - $ref: "#/components/responses/AccountAdminRequired" - "422": - $ref: "#/components/responses/ValidationError" + $ref: "#/components/schemas/Error" + example: + errors: + - code: 403 + message: "The compliance report is not available on this plan" "429": $ref: "#/components/responses/RateLimited" - /tcf-vendors/{id}: + # banner templates + /banner-templates: get: - summary: Get TCF vendor - description: Get TCF vendor by ID - operationId: getTcfVendor + summary: List banner templates + description: | + The jurisdiction templates a banner can be created from, grouped by + region. Use a template's `id` as the `jurisdiction_id` for + `POST /consent-banners/from-template`. + operationId: listBannerTemplates tags: - - TCF Vendors - parameters: - - $ref: "#/components/parameters/id" + - Consent Banners responses: "200": - description: TCF vendor + description: Templates grouped by region content: application/json: schema: - $ref: "#/components/schemas/TcfVendorResponse" + $ref: "#/components/schemas/BannerTemplatesResponse" "401": $ref: "#/components/responses/AccountTokenRequired" - "404": - $ref: "#/components/responses/NotFound" "429": $ref: "#/components/responses/RateLimited" - delete: - summary: Remove a TCF vendor + /consent-banners/from-template: + post: + summary: Create consent banner from a template description: | - Remove a vendor from the account. The record is soft-deleted, so adding - the vendor again later restores it. - operationId: deleteTcfVendor + Create a banner preconfigured for a jurisdiction — the consent mode, + targeting and copy that regulation requires. As with every banner this + API creates, it starts disabled. + + `jurisdiction_id` comes from `GET /banner-templates`. + operationId: createConsentBannerFromTemplate tags: - - TCF Vendors - parameters: - - $ref: "#/components/parameters/id" + - Consent Banners + requestBody: + description: The jurisdiction to build the banner for + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/BannerFromTemplateRequest" responses: - "200": - description: TCF vendor removed + "201": + description: The created consent banner content: application/json: schema: - $ref: "#/components/schemas/DeletedMessage" - example: - message: "TCF vendor was deleted successfully" + $ref: "#/components/schemas/ConsentBannerResponse" + "400": + $ref: "#/components/responses/AmbiguousAccount" "401": $ref: "#/components/responses/AccountTokenRequired" "403": $ref: "#/components/responses/AccountAdminRequired" "404": - $ref: "#/components/responses/NotFound" + description: No template exists for that `jurisdiction_id` + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + "422": + $ref: "#/components/responses/ValidationError" "429": $ref: "#/components/responses/RateLimited" @@ -1784,6 +1787,20 @@ components: - code: 401 message: "API key must belong to a partner" + AmbiguousAccount: + description: | + A user API key that can reach more than one account attempted a write + without saying which account it meant. Pass `account_id` (the account's + API id) as a query parameter. Account keys are never ambiguous. + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + example: + errors: + - code: 400 + message: "Specify account_id when the API key can access more than one account" + AccountAdminRequired: description: | The token resolved to a user who is not an admin of this account. @@ -3432,77 +3449,137 @@ components: data: $ref: "#/components/schemas/Scan" - TcfVendorWrite: - type: object - properties: - global_vendor_id: - type: integer - description: ID of the vendor in the IAB Global Vendor List - required: - - global_vendor_id - - TcfVendorWriteRequest: - type: object - properties: - tcf_vendor: - $ref: "#/components/schemas/TcfVendorWrite" - required: - - tcf_vendor - example: - tcf_vendor: - global_vendor_id: 755 - - TcfVendor: + ComplianceJurisdiction: type: object properties: id: type: string - readOnly: true - global_vendor_id: - type: integer - status: + description: Identifier for the regulation, e.g. `gdpr` + name: type: string - enum: [active, removed] - readOnly: true - vendor: + region: + type: string + flag: + type: [string, "null"] + applicability: + type: [string, "null"] + description: Who the regulation applies to + legal_refs: + type: [array, "null"] + items: + type: object + additionalProperties: true + compliant: + type: boolean + description: Whether the banner covering this regulation satisfies it + issues: + type: array + description: Requirements not currently met + items: + type: object + additionalProperties: true + warnings: + type: array + description: Things worth attention that do not by themselves fail the check + items: + type: object + additionalProperties: true + compliant_items: + type: array + description: Requirements already satisfied + items: + type: object + additionalProperties: true + matched_region: + type: [string, "null"] + description: The targeted region that caused this banner to be selected + banner: type: object - readOnly: true description: | - Details from the IAB Global Vendor List. Empty when the ID has been - retired upstream, since the list is not stored per account. + The banner evaluated for this regulation. Both fields are null when + no banner targets it, which is itself a compliance issue. properties: + id: + type: [string, "null"] name: type: [string, "null"] - urls: - type: [array, "null"] + + ComplianceReportResponse: + type: object + properties: + data: + type: object + properties: + compliant: + type: boolean + description: | + True when every regulation not ignored is satisfied. + jurisdictions: + type: array items: - type: object - additionalProperties: true - created_at: + $ref: "#/components/schemas/ComplianceJurisdiction" + ignored_jurisdictions: + type: array + description: | + Regulations the account has chosen to ignore. Reported so a + client can see what was excluded from `compliant`. + items: + $ref: "#/components/schemas/ComplianceJurisdiction" + + BannerTemplate: + type: object + properties: + id: type: string - format: date-time - readOnly: true - updated_at: + description: Pass as `jurisdiction_id` when creating a banner + name: type: string - format: date-time - readOnly: true - example: - id: "tcf1" - global_vendor_id: 755 - status: "active" - vendor: - name: "Google Advertising Products" - urls: - - langId: "en" - privacy: "https://policies.google.com/privacy" - created_at: "2023-01-05T10:15:30Z" - updated_at: "2023-01-05T10:15:30Z" + flag: + type: [string, "null"] + consent_type: + type: string + description: e.g. `opt_in`, `opt_out` + consent_type_label: + type: string + description: e.g. `Opt-in` - TcfVendorResponse: + BannerTemplatesResponse: type: object properties: data: - $ref: "#/components/schemas/TcfVendor" + type: array + items: + type: object + properties: + region: + type: string + region_label: + type: string + templates: + type: array + items: + $ref: "#/components/schemas/BannerTemplate" + example: + data: + - region: "europe" + region_label: "Europe" + templates: + - id: "gdpr" + name: "GDPR" + flag: "🇪🇺" + consent_type: "opt_in" + consent_type_label: "Opt-in" + + BannerFromTemplateRequest: + type: object + properties: + jurisdiction_id: + type: string + description: A template `id` from `GET /banner-templates` + required: + - jurisdiction_id + example: + jurisdiction_id: "gdpr" security: - authorization: [] From 9dbb40598ea691c1b2d3271c50ac798a5a32a7d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:16:36 +0000 Subject: [PATCH 05/19] Declare account_id, and document from-template as an unwrapped body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps from review, both in what the last commit documented rather than in the API itself. The 400 told callers to pass account_id, but no operation declared the parameter and none existed to reference — so a generated client had no way to send the selector the error asks for, leaving those writes unusable for a multi-account user key. Adds a reusable account_id query parameter and references it from all 43 account-scoped operations, reads included: the selector is resolved for any request made with a user key, not only writes. POST /consent-banners/from-template takes jurisdiction_id at the top level, while the spec said every resource endpoint wraps its body and named POST /auth as the only exception. Documented as a second exception rather than wrapped: jurisdiction_id selects which template to build from and is not an attribute of the banner being created, so a consent_banner wrapper would misrepresent it. Both statements of the contract — the spec description and the authentication page — now name both exceptions, and the authentication page explains how to choose an account. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- api-reference/authentication.mdx | 12 +++++- openapi.yaml | 74 +++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/api-reference/authentication.mdx b/api-reference/authentication.mdx index 433df0b..fe73391 100644 --- a/api-reference/authentication.mdx +++ b/api-reference/authentication.mdx @@ -92,7 +92,17 @@ Resource endpoints expect the resource wrapped under its own key rather than at The wrapper matches the resource: `category`, `storage_item`, `group`, `account_user`, `account_invitation`, and `user` for [Change password](/api-reference/auth/change-password). -[Log in](/api-reference/auth/login) is the exception — it takes `email` and `password` at the top level, unwrapped. +Two endpoints take their body unwrapped: [Log in](/api-reference/auth/login), which takes `email` and `password` at the top level, and [Create consent banner from a template](/api-reference/consent-banners/create-consent-banner-from-template), which takes `jurisdiction_id` — a selector for which template to build from, not an attribute of the banner it creates. + +## Choosing an account + +An account API key identifies exactly one account, so there is nothing to choose. A user API key can reach several, and account-scoped endpoints accept an `account_id` query parameter — the account's API id — to say which one you mean. + +For reads it is optional and narrows the request. For writes it is required whenever the key can reach more than one account: without it the request would act on an account it picked rather than one you named, so it is refused with a `400`. + +``` +POST /api/v1/consent-banners?account_id=aBcD1234 +``` ## Response Envelopes diff --git a/openapi.yaml b/openapi.yaml index 7418bf5..7849abb 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -8,8 +8,11 @@ info: Resource endpoints expect the resource wrapped under its own key, e.g. `{"category": {"name": "Analytics"}}` rather than a bare object. - `PUT /password` wraps in `user`. `POST /auth` is the exception: it takes - `email` and `password` at the top level. + `PUT /password` wraps in `user`. Two endpoints take their body unwrapped: + `POST /auth`, which takes `email` and `password` at the top level, and + `POST /consent-banners/from-template`, which takes `jurisdiction_id` — + a selector for which template to build from, not an attribute of the + banner it creates. ## Response envelopes @@ -140,6 +143,7 @@ paths: tags: - Categories parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -164,6 +168,8 @@ paths: $ref: "#/components/responses/RateLimited" post: + parameters: + - $ref: "#/components/parameters/account_id" summary: Create category description: Create a new category for the current account. operationId: createCategory @@ -198,6 +204,7 @@ paths: tags: - Categories parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -220,6 +227,7 @@ paths: tags: - Categories parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Category to update @@ -251,6 +259,7 @@ paths: tags: - Categories parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -277,6 +286,7 @@ paths: tags: - Storage Items parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -301,6 +311,8 @@ paths: $ref: "#/components/responses/RateLimited" post: + parameters: + - $ref: "#/components/parameters/account_id" summary: Create storage item description: Create a new storage item for the current account. operationId: createStorageItem @@ -341,6 +353,7 @@ paths: tags: - Storage Items parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -363,6 +376,7 @@ paths: tags: - Storage Items parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Storage item to update @@ -394,6 +408,7 @@ paths: tags: - Storage Items parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -539,6 +554,7 @@ paths: tags: - Consents parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -570,6 +586,7 @@ paths: tags: - Consents parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -596,6 +613,7 @@ paths: tags: - AI Actions parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -627,6 +645,7 @@ paths: tags: - AI Actions parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -755,6 +774,7 @@ paths: tags: - Account Users parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -786,6 +806,7 @@ paths: tags: - Account Users parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -808,6 +829,7 @@ paths: tags: - Account Users parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Account user to update @@ -841,6 +863,7 @@ paths: tags: - Account Users parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -876,6 +899,7 @@ paths: tags: - Account Invitations parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -900,6 +924,8 @@ paths: $ref: "#/components/responses/RateLimited" post: + parameters: + - $ref: "#/components/parameters/account_id" summary: Create account invitation description: | Invite someone to the current account and email them the invitation. @@ -946,6 +972,7 @@ paths: tags: - Account Invitations parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -968,6 +995,7 @@ paths: tags: - Account Invitations parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Invitation to update @@ -999,6 +1027,7 @@ paths: tags: - Account Invitations parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -1027,6 +1056,7 @@ paths: tags: - Services parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -1051,6 +1081,8 @@ paths: $ref: "#/components/responses/RateLimited" post: + parameters: + - $ref: "#/components/parameters/account_id" summary: Create service description: | Create a service under one of the account's categories. `category_id` @@ -1097,6 +1129,7 @@ paths: tags: - Services parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -1121,6 +1154,7 @@ paths: tags: - Services parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Service to update @@ -1156,6 +1190,7 @@ paths: tags: - Services parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -1188,6 +1223,7 @@ paths: tags: - Consent Banners parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -1212,6 +1248,8 @@ paths: $ref: "#/components/responses/RateLimited" post: + parameters: + - $ref: "#/components/parameters/account_id" summary: Create consent banner description: | Create a consent banner. It inherits the design of the account's most @@ -1258,6 +1296,7 @@ paths: tags: - Consent Banners parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -1282,6 +1321,7 @@ paths: tags: - Consent Banners parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Consent banner to update @@ -1317,6 +1357,7 @@ paths: tags: - Consent Banners parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -1348,6 +1389,7 @@ paths: tags: - Consent Banners parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "201": @@ -1384,6 +1426,7 @@ paths: tags: - Integrations parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -1415,6 +1458,7 @@ paths: tags: - Integrations parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -1448,6 +1492,7 @@ paths: tags: - Integrations parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Integration settings to update @@ -1487,6 +1532,7 @@ paths: tags: - Scans parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -1511,6 +1557,8 @@ paths: $ref: "#/components/responses/RateLimited" post: + parameters: + - $ref: "#/components/parameters/account_id" summary: Start a scan description: | Queue a scan of the account's website. Only one scan can run at a time @@ -1557,6 +1605,7 @@ paths: tags: - Scans parameters: + - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -1575,6 +1624,8 @@ paths: # compliance report /compliance: get: + parameters: + - $ref: "#/components/parameters/account_id" summary: Get the regional compliance report description: | Evaluate the account's banners against every regulation CookieChimp @@ -1614,6 +1665,8 @@ paths: # banner templates /banner-templates: get: + parameters: + - $ref: "#/components/parameters/account_id" summary: List banner templates description: | The jurisdiction templates a banner can be created from, grouped by @@ -1636,6 +1689,8 @@ paths: /consent-banners/from-template: post: + parameters: + - $ref: "#/components/parameters/account_id" summary: Create consent banner from a template description: | Create a banner preconfigured for a jurisdiction — the consent mode, @@ -1685,6 +1740,21 @@ components: description: API token obtained from the login endpoint or the dashboard parameters: + account_id: + name: account_id + in: query + description: | + Which account to act on, as the account's API id. Only meaningful for a + user API key, which may reach several accounts — an account key already + identifies exactly one and ignores this. + + Optional for reads, where it narrows the request to that account. + Required for writes when the key can reach more than one account: + without it the request would act on an account it picked rather than one + you named, so it is refused with a `400`. + required: false + schema: + type: string id: name: id in: path From 071c5a45f70e539191cc7961fa5463317d0bae8f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:24:59 +0000 Subject: [PATCH 06/19] Scope the account selector, and document translations and Canadian targeting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the code changes in CookieChimp#2554. The account_id selector is now opt-in per controller rather than global, so it is declared only on the endpoints that honour it — services, consent banners, banner templates, compliance, scans and integrations. Removed from the 23 legacy operations that never accepted it; those keep resolving an account on their own, and none of them can return the ambiguous-account 400. The banner write schema gains banner_languages_attributes. The create operation tells callers to add translations by updating the banner, but the schema described no way to do it, so the generated reference and typed clients had nothing to show. Documents adding, updating by id, and discarding. Both banner schemas gain target_canadian_regions. Canadian provinces share the target_us_states column behind a virtual accessor, so a Canadian-targeted banner previously reported QC and ON as US states and never returned the field that accepts them — its targeting could not be round-tripped. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- api-reference/authentication.mdx | 2 +- openapi.yaml | 84 ++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/api-reference/authentication.mdx b/api-reference/authentication.mdx index fe73391..da91f9c 100644 --- a/api-reference/authentication.mdx +++ b/api-reference/authentication.mdx @@ -96,7 +96,7 @@ Two endpoints take their body unwrapped: [Log in](/api-reference/auth/login), wh ## Choosing an account -An account API key identifies exactly one account, so there is nothing to choose. A user API key can reach several, and account-scoped endpoints accept an `account_id` query parameter — the account's API id — to say which one you mean. +An account API key identifies exactly one account, so there is nothing to choose. A user API key can reach several, and the endpoints that declare an `account_id` query parameter — the account's API id — let you say which one you mean. Endpoints that don't declare it act on the account the key resolves to on its own. For reads it is optional and narrows the request. For writes it is required whenever the key can reach more than one account: without it the request would act on an account it picked rather than one you named, so it is refused with a `400`. diff --git a/openapi.yaml b/openapi.yaml index 7849abb..b0ce9ef 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -143,7 +143,6 @@ paths: tags: - Categories parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -168,8 +167,6 @@ paths: $ref: "#/components/responses/RateLimited" post: - parameters: - - $ref: "#/components/parameters/account_id" summary: Create category description: Create a new category for the current account. operationId: createCategory @@ -204,7 +201,6 @@ paths: tags: - Categories parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -227,7 +223,6 @@ paths: tags: - Categories parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Category to update @@ -259,7 +254,6 @@ paths: tags: - Categories parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -286,7 +280,6 @@ paths: tags: - Storage Items parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -311,8 +304,6 @@ paths: $ref: "#/components/responses/RateLimited" post: - parameters: - - $ref: "#/components/parameters/account_id" summary: Create storage item description: Create a new storage item for the current account. operationId: createStorageItem @@ -353,7 +344,6 @@ paths: tags: - Storage Items parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -376,7 +366,6 @@ paths: tags: - Storage Items parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Storage item to update @@ -408,7 +397,6 @@ paths: tags: - Storage Items parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -554,7 +542,6 @@ paths: tags: - Consents parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -586,7 +573,6 @@ paths: tags: - Consents parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -613,7 +599,6 @@ paths: tags: - AI Actions parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -645,7 +630,6 @@ paths: tags: - AI Actions parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -774,7 +758,6 @@ paths: tags: - Account Users parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -806,7 +789,6 @@ paths: tags: - Account Users parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -829,7 +811,6 @@ paths: tags: - Account Users parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Account user to update @@ -863,7 +844,6 @@ paths: tags: - Account Users parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -899,7 +879,6 @@ paths: tags: - Account Invitations parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/per_page" - $ref: "#/components/parameters/page" responses: @@ -924,8 +903,6 @@ paths: $ref: "#/components/responses/RateLimited" post: - parameters: - - $ref: "#/components/parameters/account_id" summary: Create account invitation description: | Invite someone to the current account and email them the invitation. @@ -972,7 +949,6 @@ paths: tags: - Account Invitations parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -995,7 +971,6 @@ paths: tags: - Account Invitations parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" requestBody: description: Invitation to update @@ -1027,7 +1002,6 @@ paths: tags: - Account Invitations parameters: - - $ref: "#/components/parameters/account_id" - $ref: "#/components/parameters/id" responses: "200": @@ -1748,6 +1722,9 @@ components: user API key, which may reach several accounts — an account key already identifies exactly one and ignores this. + Accepted by the endpoints that declare it; others act on the account the + key resolves to on its own. + Optional for reads, where it narrows the request to that account. Required for writes when the key can reach more than one account: without it the request would act on an account it picked rather than one @@ -3217,6 +3194,54 @@ components: type: array items: type: string + target_canadian_regions: + type: array + items: + type: string + description: | + Province and territory codes, used when `target_type` is + `selected_canadian_provinces`. + banner_languages_attributes: + type: object + description: | + Create, update or discard translations. Not accepted when creating a + banner — add them by updating it, which is where the account's + language entitlement is enforced. + + Keys are arbitrary indices. Omit `id` to add a translation; pass the + `id` from the banner's `translations` array to change an existing + one, and set `discarded_at` to remove it. + additionalProperties: + type: object + properties: + id: + type: string + description: From the banner's `translations` array + language_code: + type: string + description: ISO 639-1 code + consent_title: + type: string + consent_description: + type: string + banner_accept_btn_text: + type: string + banner_accept_necessary_btn_text: + type: string + banner_preferences_btn_text: + type: string + settings_title: + type: string + settings_description: + type: string + privacy_policy_url: + type: string + save_preferences_btn_text: + type: string + discarded_at: + type: [string, "null"] + format: date-time + description: Set to remove this translation ConsentBannerWriteRequest: type: object @@ -3284,6 +3309,13 @@ components: type: array items: type: string + target_canadian_regions: + type: array + items: + type: string + description: | + Populated instead of `target_us_states` when `target_type` is + `selected_canadian_provinces`; the two share a column. translations: type: array readOnly: true From 1c5ac046a57b5b5edcf4e72e89f452cc771c002d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:29:53 +0000 Subject: [PATCH 07/19] Split banner create from update, and scope the selector requirement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both from review, and both fallout from scoping the account selector in the last commit. POST and PUT /consent-banners shared one schema, which since the last commit advertised banner_languages_attributes — a field create does not accept, and says so in the same breath. A generated client would have offered a payload the API ignores. Create now takes ConsentBannerCreateRequest and update takes ConsentBannerUpdateRequest, which composes the shared write schema with the translation attributes. Checked against a JSON Schema validator: an update carrying translations validates, and both plain payloads still do. The parameter description and the authentication page both said writes require account_id, full stop. That was true when the selector was global; now that it is declared per endpoint, it binds only the operations that accept it — the rest resolve an account on their own, as the sentence immediately before it already said. Qualified both. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- api-reference/authentication.mdx | 2 +- openapi.yaml | 29 +++++++++++++++++++++++------ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/api-reference/authentication.mdx b/api-reference/authentication.mdx index da91f9c..1d70dc8 100644 --- a/api-reference/authentication.mdx +++ b/api-reference/authentication.mdx @@ -98,7 +98,7 @@ Two endpoints take their body unwrapped: [Log in](/api-reference/auth/login), wh An account API key identifies exactly one account, so there is nothing to choose. A user API key can reach several, and the endpoints that declare an `account_id` query parameter — the account's API id — let you say which one you mean. Endpoints that don't declare it act on the account the key resolves to on its own. -For reads it is optional and narrows the request. For writes it is required whenever the key can reach more than one account: without it the request would act on an account it picked rather than one you named, so it is refused with a `400`. +Where an endpoint accepts it, reads treat it as optional — it just narrows the request — while writes require it whenever the key can reach more than one account: without it the request would act on an account it picked rather than one you named, so it is refused with a `400`. Endpoints that don't accept the parameter are unaffected either way. ``` POST /api/v1/consent-banners?account_id=aBcD1234 diff --git a/openapi.yaml b/openapi.yaml index b0ce9ef..73f858a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1243,7 +1243,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ConsentBannerWriteRequest" + $ref: "#/components/schemas/ConsentBannerCreateRequest" responses: "201": description: Created consent banner @@ -1303,7 +1303,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ConsentBannerWriteRequest" + $ref: "#/components/schemas/ConsentBannerUpdateRequest" responses: "200": description: Updated consent banner @@ -1726,9 +1726,9 @@ components: key resolves to on its own. Optional for reads, where it narrows the request to that account. - Required for writes when the key can reach more than one account: - without it the request would act on an account it picked rather than one - you named, so it is refused with a `400`. + Required for writes that declare it when the key can reach more than one + account: without it the request would act on an account it picked rather + than one you named, so it is refused with a `400`. required: false schema: type: string @@ -3201,6 +3201,12 @@ components: description: | Province and territory codes, used when `target_type` is `selected_canadian_provinces`. + ConsentBannerUpdate: + description: | + Everything a banner accepts on update, including its translations. + allOf: + - $ref: "#/components/schemas/ConsentBannerWrite" + properties: banner_languages_attributes: type: object description: | @@ -3243,13 +3249,24 @@ components: format: date-time description: Set to remove this translation - ConsentBannerWriteRequest: + ConsentBannerCreateRequest: type: object + description: | + Translations are not accepted here — add them by updating the banner, + which is where the account's language entitlement is enforced. properties: consent_banner: $ref: "#/components/schemas/ConsentBannerWrite" required: - consent_banner + + ConsentBannerUpdateRequest: + type: object + properties: + consent_banner: + $ref: "#/components/schemas/ConsentBannerUpdate" + required: + - consent_banner example: consent_banner: name: "EU banner" From 5740b591c4757f0deeb3add3f7106b68fc7a31f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:37:52 +0000 Subject: [PATCH 08/19] List every request wrapper, including the ones that don't match the path The guide said the wrapper matches the resource and listed six of them. Six more were added by this PR, and one of those does not follow the rule the sentence states: /services wraps in site_service, not service. A caller following the guide would have sent the wrong shape. Replaces the prose list with a table covering every endpoint that takes a wrapped body, and says outright that the wrapper does not always match the path. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- api-reference/authentication.mdx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/api-reference/authentication.mdx b/api-reference/authentication.mdx index 1d70dc8..8da5e34 100644 --- a/api-reference/authentication.mdx +++ b/api-reference/authentication.mdx @@ -90,7 +90,21 @@ Resource endpoints expect the resource wrapped under its own key rather than at } ``` -The wrapper matches the resource: `category`, `storage_item`, `group`, `account_user`, `account_invitation`, and `user` for [Change password](/api-reference/auth/change-password). +The wrapper matches the resource, though not always the path — `/services` wraps in `site_service`: + +| Endpoint | Wrapper | +|---|---| +| `/categories` | `category` | +| `/storage-items` | `storage_item` | +| `/services` | `site_service` | +| `/consent-banners` | `consent_banner` | +| `/scans` | `scan` | +| `/integrations` | `integration` | +| `/accounts` | `account` | +| `/groups` | `group` | +| `/account-users` | `account_user` | +| `/account-invitations` | `account_invitation` | +| [Change password](/api-reference/auth/change-password) | `user` | Two endpoints take their body unwrapped: [Log in](/api-reference/auth/login), which takes `email` and `password` at the top level, and [Create consent banner from a template](/api-reference/consent-banners/create-consent-banner-from-template), which takes `jurisdiction_id` — a selector for which template to build from, not an attribute of the banner it creates. From 18bfa8d51e4f0d7f7a88ed8ae7f29cde671cd9c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 14:43:31 +0000 Subject: [PATCH 09/19] Note that banner templates are not a paginated listing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both envelope statements said list endpoints return {"pagination": ..., "data": [...]}, and the pagination section said list endpoints accept page and per_page. GET /banner-templates does neither: it is a fixed catalogue of jurisdiction templates rather than a collection of the account's records, so it returns data alone and takes no pagination parameters. A client generated against the stated contract could have required a pagination object that is never sent. Checked the rest of the spec for the same mismatch — it is the only listing without pagination. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- api-reference/authentication.mdx | 5 ++++- openapi.yaml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/api-reference/authentication.mdx b/api-reference/authentication.mdx index 8da5e34..a92b5b1 100644 --- a/api-reference/authentication.mdx +++ b/api-reference/authentication.mdx @@ -66,7 +66,7 @@ If you exceed a limit you receive a `429 Too Many Requests` response with a `Ret ## Pagination -List endpoints support pagination with the following query parameters: +List endpoints support pagination with the following query parameters. [Banner templates](/api-reference/consent-banners/list-banner-templates) is the one exception — it returns the whole catalogue and accepts neither parameter. | Parameter | Description | Default | |-----------|-------------|---------| @@ -123,6 +123,7 @@ POST /api/v1/consent-banners?account_id=aBcD1234 | Response | Shape | |----------|-------| | List endpoints | `{ "pagination": { ... }, "data": [ ... ] }` | +| [Banner templates](/api-reference/consent-banners/list-banner-templates) | `{ "data": [ ... ] }` — no pagination | | Single resource | `{ "data": { ... } }` | | A single group or account | The resource itself, unwrapped | | Resource-deletion endpoints | `{ "message": "..." }` | @@ -130,6 +131,8 @@ POST /api/v1/consent-banners?account_id=aBcD1234 Group and account **listings** use the standard list envelope in the first row — only their single-resource responses are unwrapped. +Banner templates are a fixed catalogue rather than a collection of the account's own records, so that endpoint is not paginated and returns no `pagination` object. + ## Errors The API returns standard HTTP status codes. diff --git a/openapi.yaml b/openapi.yaml index 73f858a..3e705f8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -17,7 +17,10 @@ info: ## Response envelopes Listing endpoints — including `GET /groups` and `GET /accounts` — return - `{"pagination": {...}, "data": [...]}`. Most single-resource endpoints + `{"pagination": {...}, "data": [...]}`. `GET /banner-templates` is the + exception: it returns `{"data": [...]}` with no pagination, being a fixed + catalogue rather than a collection of the account's records. + Most single-resource endpoints return `{"data": {...}}`; a single group or account is returned unwrapped. Resource-deletion endpoints return `{"message": "..."}`; `DELETE /auth` returns an empty object. From 20049b592db6983c1d903fc6cb57276785d13772 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:06:24 +0000 Subject: [PATCH 10/19] Return the account settings and translation copy the API accepts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows CookieChimp#2554. The account response documented four fields while PATCH accepts twelve, so a client could write default_language, translations, industry, description, support_email, banner_debug_mode, blocking_mode, retain_consent_duration, domain or subdomain and had no way to read any of them back. All are in the response schema now. allowed_domains_list stays write-only and says so — the read side returns the parsed allowed_domains array. Banner translations carried an id and a language code while the update endpoint accepts seventeen localized fields. The copy is documented alongside the id, so the reference shows what can be read before it is overwritten. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- openapi.yaml | 75 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 3e705f8..a5a13a0 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2878,6 +2878,37 @@ components: type: string readOnly: true description: Domain of the account + account_type: + type: string + enum: [website, mobile_app] + readOnly: true + domain: + type: [string, "null"] + subdomain: + type: [string, "null"] + default_language: + type: string + description: ISO 639-1 code the banner falls back to + auto_translate: + type: boolean + translations: + type: array + items: + type: string + description: ISO 639-1 codes the banner is translated into + industry: + type: [string, "null"] + description: + type: [string, "null"] + support_email: + type: [string, "null"] + banner_debug_mode: + type: boolean + blocking_mode: + type: [string, "null"] + retain_consent_duration: + type: integer + description: Years consent records are retained allowed_domains: type: array items: @@ -2965,7 +2996,9 @@ components: description: ISO 639-1 codes the banner is translated into allowed_domains_list: type: string - description: Comma-separated list of domains the script may run on + description: | + Comma-separated list of domains the script may run on. Write-only — + responses return the parsed `allowed_domains` array instead. industry: type: string description: @@ -3340,8 +3373,10 @@ components: type: array readOnly: true description: | - The banner's translations. The `id` is what you send back in - `banner_languages_attributes` to update or discard one. + The banner's translations, each with its localized copy alongside + its `id` — so a client can read what it is allowed to write rather + than overwriting fields it cannot see. The `id` is what you send + back in `banner_languages_attributes` to update or discard one. items: type: object properties: @@ -3350,6 +3385,40 @@ components: language_code: type: string description: ISO 639-1 code + consent_title: + type: [string, "null"] + consent_description: + type: [string, "null"] + consent_additional_description: + type: [string, "null"] + banner_accept_btn_text: + type: [string, "null"] + banner_accept_necessary_btn_text: + type: [string, "null"] + banner_preferences_btn_text: + type: [string, "null"] + banner_footer_content: + type: [string, "null"] + settings_title: + type: [string, "null"] + settings_description: + type: [string, "null"] + settings_more_info_title: + type: [string, "null"] + settings_more_info: + type: [string, "null"] + privacy_policy_url: + type: [string, "null"] + do_not_sell_title: + type: [string, "null"] + do_not_sell_description: + type: [string, "null"] + preferences_accept_btn_text: + type: [string, "null"] + preferences_accept_necessary_btn_text: + type: [string, "null"] + save_preferences_btn_text: + type: [string, "null"] created_at: type: string format: date-time From 91f4521301bd76b7e41202f91b6f0b3f9f721274 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:11:29 +0000 Subject: [PATCH 11/19] Document every writable translation field, not most of them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The banner update schema listed twelve of the twenty attributes Rails permits under banner_languages_attributes, so eight supported edits were missing: consent_additional_description, banner_footer_content, settings_more_info_title, settings_more_info, do_not_sell_title, do_not_sell_description, preferences_accept_btn_text and preferences_accept_necessary_btn_text. The response was expanded to return all of them last commit, which left the reference telling clients to read fields it never showed them how to write. Reconciled against ConsentBannerPolicy rather than by inspection — the write schema, the read schema and the permit list now agree with nothing missing and nothing spurious on either side, and every field the API returns is one it accepts. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- openapi.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index a5a13a0..c669fb5 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3280,6 +3280,22 @@ components: type: string save_preferences_btn_text: type: string + consent_additional_description: + type: string + banner_footer_content: + type: string + settings_more_info_title: + type: string + settings_more_info: + type: string + do_not_sell_title: + type: string + do_not_sell_description: + type: string + preferences_accept_btn_text: + type: string + preferences_accept_necessary_btn_text: + type: string discarded_at: type: [string, "null"] format: date-time From d291d13cb13e91d4e711f20254f0e5b10a7dbd57 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:21:10 +0000 Subject: [PATCH 12/19] Document every banner setting, and the failure modes this PR introduced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The banner write schema listed eighteen of the forty-nine settings the designer exposes and waved at the rest with "any other attribute the designer exposes is accepted too". That was tolerable while the response returned a subset as well; now that CookieChimp#2554 returns all of them, it left the reference describing neither side properly. Generated from the model's columns and enums, so the listed types match what the API sends. The read schema now composes the write schema rather than restating part of it, which mirrors how the response is built — 55 effective properties, with nothing writable that cannot be read back. ConsentBannerUpdate had allOf beside a sibling properties block, which left its own example failing validation because the inherited fields counted as unevaluated. Both branches sit inside the allOf now, and a full update payload carrying targeting, a logo and a nested translation validates. Translation update fields accept null. The response returns null for unset localized copy, so string-only request properties meant the object could not be sent back as received. The errors table defined 400 as an invalid page parameter alone and did not mention 403 at all, while this PR added an account-ambiguity 400 and an admin-only 403. Both are in the table now. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- api-reference/authentication.mdx | 3 +- openapi.yaml | 469 +++++++++++++++++-------------- 2 files changed, 266 insertions(+), 206 deletions(-) diff --git a/api-reference/authentication.mdx b/api-reference/authentication.mdx index a92b5b1..57d79bf 100644 --- a/api-reference/authentication.mdx +++ b/api-reference/authentication.mdx @@ -141,8 +141,9 @@ The API returns standard HTTP status codes. |-------------|-------------| | `200` | Success | | `201` | Created | -| `400` | Bad request — an invalid `page` parameter | +| `400` | Bad request — an invalid `page` parameter, or a write from a user key that can reach more than one account without an `account_id` | | `401` | Unauthorized — invalid or missing API token, or a token of the wrong type | +| `403` | Forbidden — the key resolved to a user who is not an admin of the account, on a change the dashboard reserves for admins | | `404` | Not found | | `422` | Validation error | | `429` | Rate limit exceeded | diff --git a/openapi.yaml b/openapi.yaml index c669fb5..16f3baf 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3177,129 +3177,182 @@ components: ConsentBannerWrite: type: object description: | - The banner attributes the dashboard's banner designer can set. The full - set is large; the commonly used ones are listed here, and any other - attribute the designer exposes is accepted too. + Every setting the banner designer exposes. The response returns the same + set, so a banner can be read, edited and sent back unchanged. properties: - name: + banner_accept_btn_text: type: string - enabled: - type: boolean - description: Whether the banner is live. New banners are always created disabled. - enable_tcf: - type: boolean - description: Whether the banner runs in IAB TCF mode - force_consent: - type: boolean - description: Whether the page is blocked until the visitor chooses - consent_mode: + banner_accept_necessary_btn_text: + type: string + banner_footer_content: + type: string + banner_preferences_btn_text: + type: string + btn_border_radius: + type: string + consent_additional_description: + type: string + consent_button_placement: + type: string + consent_description: type: string - description: e.g. `opt_in`, `opt_out` consent_duration_days: type: integer description: How long a visitor's choice is remembered consent_layout: type: string description: e.g. `box`, `bar` + consent_mode: + type: string + description: e.g. `opt_in`, `opt_out` consent_position: type: string description: e.g. `bottom_center` consent_title: type: string - consent_description: + dark_mode: type: string - privacy_policy_url: + description: e.g. `light`, `dark`, `auto` + do_not_sell_description: + type: string + do_not_sell_title: + type: string + enable_banner_footer: + type: boolean + enable_tcf: + type: boolean + description: Whether the banner runs in IAB TCF mode + enabled: + type: boolean + description: Whether the banner is live. New banners are always created disabled. + equal_weight_buttons: + type: boolean + floating_preferences_icon_position: + type: string + force_consent: + type: boolean + description: Whether the page is blocked until the visitor chooses + language_autodetect: + type: string + logo_url_dark: + type: string + logo_url_light: + type: string + modal_border_radius: + type: string + name: + type: string + preferences_accept_btn_text: + type: string + preferences_accept_necessary_btn_text: type: string preferences_control_level: type: string description: How granular the preferences modal is - dark_mode: + privacy_policy_url: type: string - description: e.g. `light`, `dark`, `auto` - language_autodetect: + save_preferences_btn_text: + type: string + settings_description: + type: string + settings_layout: + type: string + settings_more_info: + type: string + settings_more_info_title: type: string + settings_position_x: + type: string + settings_title: + type: string + show_banner_accept_btn: + type: boolean + show_banner_accept_necessary_btn: + type: boolean + show_banner_close_btn: + type: boolean + show_banner_preferences_btn: + type: boolean + show_floating_preferences_icon: + type: boolean + show_preferences_accept_btn: + type: boolean + show_preferences_accept_necessary_btn: + type: boolean + show_save_preferences_btn: + type: boolean + show_services: + type: boolean + swap_buttons: + type: boolean target_type: type: string description: Who sees the banner, e.g. `worldwide`, `selected_countries` - target_countries: - type: array - items: - type: string - description: ISO 3166-1 alpha-2 codes - target_us_states: - type: array - items: - type: string - target_canadian_regions: - type: array - items: - type: string - description: | - Province and territory codes, used when `target_type` is - `selected_canadian_provinces`. + ConsentBannerUpdate: description: | Everything a banner accepts on update, including its translations. allOf: - $ref: "#/components/schemas/ConsentBannerWrite" - properties: - banner_languages_attributes: - type: object - description: | - Create, update or discard translations. Not accepted when creating a - banner — add them by updating it, which is where the account's - language entitlement is enforced. - - Keys are arbitrary indices. Omit `id` to add a translation; pass the - `id` from the banner's `translations` array to change an existing - one, and set `discarded_at` to remove it. - additionalProperties: - type: object - properties: - id: - type: string - description: From the banner's `translations` array - language_code: - type: string - description: ISO 639-1 code - consent_title: - type: string - consent_description: - type: string - banner_accept_btn_text: - type: string - banner_accept_necessary_btn_text: - type: string - banner_preferences_btn_text: - type: string - settings_title: - type: string - settings_description: - type: string - privacy_policy_url: - type: string - save_preferences_btn_text: - type: string - consent_additional_description: - type: string - banner_footer_content: - type: string - settings_more_info_title: - type: string - settings_more_info: - type: string - do_not_sell_title: - type: string - do_not_sell_description: - type: string - preferences_accept_btn_text: - type: string - preferences_accept_necessary_btn_text: - type: string - discarded_at: - type: [string, "null"] - format: date-time - description: Set to remove this translation + - type: object + properties: + banner_languages_attributes: + type: object + description: | + Create, update or discard translations. Not accepted when creating a + banner — add them by updating it, which is where the account's + language entitlement is enforced. + + Keys are arbitrary indices. Omit `id` to add a translation; pass the + `id` from the banner's `translations` array to change an existing + one, and set `discarded_at` to remove it. + additionalProperties: + type: object + properties: + id: + type: string + description: From the banner's `translations` array + language_code: + type: string + description: ISO 639-1 code + consent_title: + type: [string, "null"] + consent_description: + type: [string, "null"] + banner_accept_btn_text: + type: [string, "null"] + banner_accept_necessary_btn_text: + type: [string, "null"] + banner_preferences_btn_text: + type: [string, "null"] + settings_title: + type: [string, "null"] + settings_description: + type: [string, "null"] + privacy_policy_url: + type: [string, "null"] + save_preferences_btn_text: + type: [string, "null"] + consent_additional_description: + type: [string, "null"] + banner_footer_content: + type: [string, "null"] + settings_more_info_title: + type: [string, "null"] + settings_more_info: + type: [string, "null"] + do_not_sell_title: + type: [string, "null"] + do_not_sell_description: + type: [string, "null"] + preferences_accept_btn_text: + type: [string, "null"] + preferences_accept_necessary_btn_text: + type: [string, "null"] + discarded_at: + type: [string, "null"] + format: date-time + description: Set to remove this translation ConsentBannerCreateRequest: type: object @@ -3328,121 +3381,127 @@ components: target_countries: ["DE", "FR"] ConsentBanner: - type: object - properties: - id: - type: string - readOnly: true - name: - type: string - enabled: - type: boolean - enable_tcf: - type: boolean - force_consent: - type: boolean - consent_mode: - type: string - consent_duration_days: - type: integer - consent_layout: - type: string - consent_position: - type: string - consent_title: - type: [string, "null"] - consent_description: - type: [string, "null"] - privacy_policy_url: - type: [string, "null"] - preferences_control_level: - type: string - dark_mode: - type: string - language_autodetect: - type: string - revision: - type: integer - readOnly: true - description: Bumped when a change requires visitors to consent again - targeting: - type: object + description: | + Every setting the update endpoint accepts, plus the read-only fields + below — the response is derived from the same list the API permits, so a + banner can be read, edited and sent back unchanged. + allOf: + - $ref: "#/components/schemas/ConsentBannerWrite" + - type: object properties: - target_type: + id: type: string - target_countries: - type: array - items: - type: string - target_us_states: - type: array - items: - type: string - target_canadian_regions: + readOnly: true + name: + type: string + enabled: + type: boolean + enable_tcf: + type: boolean + force_consent: + type: boolean + consent_mode: + type: string + consent_duration_days: + type: integer + consent_layout: + type: string + consent_position: + type: string + consent_title: + type: [string, "null"] + consent_description: + type: [string, "null"] + privacy_policy_url: + type: [string, "null"] + preferences_control_level: + type: string + dark_mode: + type: string + language_autodetect: + type: string + revision: + type: integer + readOnly: true + description: Bumped when a change requires visitors to consent again + targeting: + type: object + properties: + target_type: + type: string + target_countries: + type: array + items: + type: string + target_us_states: + type: array + items: + type: string + target_canadian_regions: + type: array + items: + type: string + description: | + Populated instead of `target_us_states` when `target_type` is + `selected_canadian_provinces`; the two share a column. + translations: type: array - items: - type: string + readOnly: true description: | - Populated instead of `target_us_states` when `target_type` is - `selected_canadian_provinces`; the two share a column. - translations: - type: array - readOnly: true - description: | - The banner's translations, each with its localized copy alongside - its `id` — so a client can read what it is allowed to write rather - than overwriting fields it cannot see. The `id` is what you send - back in `banner_languages_attributes` to update or discard one. - items: - type: object - properties: - id: - type: string - language_code: - type: string - description: ISO 639-1 code - consent_title: - type: [string, "null"] - consent_description: - type: [string, "null"] - consent_additional_description: - type: [string, "null"] - banner_accept_btn_text: - type: [string, "null"] - banner_accept_necessary_btn_text: - type: [string, "null"] - banner_preferences_btn_text: - type: [string, "null"] - banner_footer_content: - type: [string, "null"] - settings_title: - type: [string, "null"] - settings_description: - type: [string, "null"] - settings_more_info_title: - type: [string, "null"] - settings_more_info: - type: [string, "null"] - privacy_policy_url: - type: [string, "null"] - do_not_sell_title: - type: [string, "null"] - do_not_sell_description: - type: [string, "null"] - preferences_accept_btn_text: - type: [string, "null"] - preferences_accept_necessary_btn_text: - type: [string, "null"] - save_preferences_btn_text: - type: [string, "null"] - created_at: - type: string - format: date-time - readOnly: true - updated_at: - type: string - format: date-time - readOnly: true + The banner's translations, each with its localized copy alongside + its `id` — so a client can read what it is allowed to write rather + than overwriting fields it cannot see. The `id` is what you send + back in `banner_languages_attributes` to update or discard one. + items: + type: object + properties: + id: + type: string + language_code: + type: string + description: ISO 639-1 code + consent_title: + type: [string, "null"] + consent_description: + type: [string, "null"] + consent_additional_description: + type: [string, "null"] + banner_accept_btn_text: + type: [string, "null"] + banner_accept_necessary_btn_text: + type: [string, "null"] + banner_preferences_btn_text: + type: [string, "null"] + banner_footer_content: + type: [string, "null"] + settings_title: + type: [string, "null"] + settings_description: + type: [string, "null"] + settings_more_info_title: + type: [string, "null"] + settings_more_info: + type: [string, "null"] + privacy_policy_url: + type: [string, "null"] + do_not_sell_title: + type: [string, "null"] + do_not_sell_description: + type: [string, "null"] + preferences_accept_btn_text: + type: [string, "null"] + preferences_accept_necessary_btn_text: + type: [string, "null"] + save_preferences_btn_text: + type: [string, "null"] + created_at: + type: string + format: date-time + readOnly: true + updated_at: + type: string + format: date-time + readOnly: true example: id: "ban1" name: "EU banner" From a15f2eea8fc84bca2624246d8e7f7170b27bc8ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:31:11 +0000 Subject: [PATCH 13/19] Restore the banner targeting fields, and fix the nullable composition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two regressions from generating the write schema last commit. The generator only emitted attributes the policy lists as bare symbols, so target_countries, target_us_states, target_canadian_regions and button_colors were dropped — the arrays a targeting update actually needs, and which the update example itself sends. All four are back, and the schema is now checked against the policy rather than against the read schema it composes, which made the previous check tautological. ConsentBanner declared those fields nullable in a second allOf branch, which does not override the string-only definition inherited from the first: every branch must validate, so a response carrying a null consent_title violated its own schema. The nullable fields are nullable in the shared definition instead, derived from which columns the database leaves nullable, and the conflicting branch is gone. Checked with a JSON Schema validator on real shapes: a response with null localized copy validates, and so does an update carrying targeting, a colour map and a nested translation with a null field. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- openapi.yaml | 67 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index 16f3baf..57813cc 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3178,24 +3178,26 @@ components: type: object description: | Every setting the banner designer exposes. The response returns the same - set, so a banner can be read, edited and sent back unchanged. + set, so a banner can be read, edited and sent back unchanged. Fields the + database leaves nullable are declared nullable here too — the response + returns null for unset copy, and the same shape has to be sendable back. properties: banner_accept_btn_text: - type: string + type: [string, "null"] banner_accept_necessary_btn_text: - type: string + type: [string, "null"] banner_footer_content: - type: string + type: [string, "null"] banner_preferences_btn_text: - type: string + type: [string, "null"] btn_border_radius: type: string consent_additional_description: - type: string + type: [string, "null"] consent_button_placement: type: string consent_description: - type: string + type: [string, "null"] consent_duration_days: type: integer description: How long a visitor's choice is remembered @@ -3209,14 +3211,14 @@ components: type: string description: e.g. `bottom_center` consent_title: - type: string + type: [string, "null"] dark_mode: type: string description: e.g. `light`, `dark`, `auto` do_not_sell_description: - type: string + type: [string, "null"] do_not_sell_title: - type: string + type: [string, "null"] enable_banner_footer: type: boolean enable_tcf: @@ -3235,36 +3237,36 @@ components: language_autodetect: type: string logo_url_dark: - type: string + type: [string, "null"] logo_url_light: - type: string + type: [string, "null"] modal_border_radius: type: string name: type: string preferences_accept_btn_text: - type: string + type: [string, "null"] preferences_accept_necessary_btn_text: - type: string + type: [string, "null"] preferences_control_level: type: string description: How granular the preferences modal is privacy_policy_url: - type: string + type: [string, "null"] save_preferences_btn_text: - type: string + type: [string, "null"] settings_description: - type: string + type: [string, "null"] settings_layout: type: string settings_more_info: - type: string + type: [string, "null"] settings_more_info_title: - type: string + type: [string, "null"] settings_position_x: type: string settings_title: - type: string + type: [string, "null"] show_banner_accept_btn: type: boolean show_banner_accept_necessary_btn: @@ -3285,9 +3287,28 @@ components: type: boolean swap_buttons: type: boolean + target_canadian_regions: + type: array + items: + type: string + description: Province and territory codes, used when `target_type` is `selected_canadian_provinces`. Shares a column with `target_us_states`. + target_countries: + type: array + items: + type: string + description: ISO 3166-1 alpha-2 codes target_type: type: string description: Who sees the banner, e.g. `worldwide`, `selected_countries` + target_us_states: + type: array + items: + type: string + button_colors: + type: object + additionalProperties: + type: string + description: Colour overrides, keyed by button element ConsentBannerUpdate: description: | @@ -3408,12 +3429,6 @@ components: type: string consent_position: type: string - consent_title: - type: [string, "null"] - consent_description: - type: [string, "null"] - privacy_policy_url: - type: [string, "null"] preferences_control_level: type: string dark_mode: From 6685e48974f97d5ba9fd1ac9e416347758575a42 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:37:12 +0000 Subject: [PATCH 14/19] Reconcile the errors table with every failure the API declares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 403 row described only a non-admin attempting an admin-only change, while GET /compliance returns 403 when the account's plan excludes the report — a read-time entitlement failure a reader would have misdiagnosed from the table. Rather than fix that row alone, enumerated every response the spec declares and reconciled the table against it, which turned up two more: - 401 also covers the wrong email or password on log in, not just a bad or absent API token. - 422 also covers a setting the plan does not allow. That one is mine from this round: CookieChimp#2554 started refusing scheduled scanning on free accounts and I documented the behaviour nowhere. The integration update operation now says so, including that turning the feature off always works. 404 gains the cross-account case, which several operations already described individually. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- api-reference/authentication.mdx | 8 ++++---- openapi.yaml | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/api-reference/authentication.mdx b/api-reference/authentication.mdx index 57d79bf..9b657e3 100644 --- a/api-reference/authentication.mdx +++ b/api-reference/authentication.mdx @@ -142,10 +142,10 @@ The API returns standard HTTP status codes. | `200` | Success | | `201` | Created | | `400` | Bad request — an invalid `page` parameter, or a write from a user key that can reach more than one account without an `account_id` | -| `401` | Unauthorized — invalid or missing API token, or a token of the wrong type | -| `403` | Forbidden — the key resolved to a user who is not an admin of the account, on a change the dashboard reserves for admins | -| `404` | Not found | -| `422` | Validation error | +| `401` | Unauthorized — invalid or missing API token, a token of the wrong type, or, on [Log in](/api-reference/auth/login), the wrong email or password | +| `403` | Forbidden — the key resolved to a user who is not an admin of the account, on a change the dashboard reserves for admins; or the account's plan does not include the feature, as with the [compliance report](/api-reference/compliance/get-compliance-report) | +| `404` | Not found — including a referenced record that belongs to another account | +| `422` | Validation error, or a setting the account's plan does not allow | | `429` | Rate limit exceeded | Authentication, pagination and not-found errors return an `errors` array of objects with `code` and `message`: diff --git a/openapi.yaml b/openapi.yaml index 57813cc..d7dde12 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1465,6 +1465,11 @@ paths: `ads_data_redaction`. Enabling an integration may add its associated vendors to the account. + + Scheduled scanning is a paid feature: on a free plan, setting + `SiteScanIntegration` to `active` or turning on `auto_add_vendors` is + refused with a `422`. Turning it off always works, so an account that + downgrades can still disable it. operationId: updateIntegration tags: - Integrations From a0cb74be6e1dc7e26f99457df051e5ef178c188a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:48:43 +0000 Subject: [PATCH 15/19] Keep enabled out of the banner creation schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /consent-banners forces a new banner disabled, but the create request reused the shared write schema and so advertised `enabled`. A generated client would have offered it, and a caller could have sent `enabled: true` and been quietly overridden — the operation said as much in prose while the schema said otherwise. Create now composes the write schema with `enabled` excluded. Checked with a JSON Schema validator: a create carrying `enabled` is rejected, one without it validates, and update still accepts it, which is how a banner goes live. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- openapi.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openapi.yaml b/openapi.yaml index d7dde12..2425e85 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3380,6 +3380,18 @@ components: format: date-time description: Set to remove this translation + ConsentBannerCreate: + description: | + The create payload. `enabled` is absent because a new banner is always + created disabled — enable it with an update once its content and + targeting are set. + allOf: + - $ref: "#/components/schemas/ConsentBannerWrite" + - type: object + properties: + enabled: + not: {} + ConsentBannerCreateRequest: type: object description: | @@ -3387,7 +3399,7 @@ components: which is where the account's language entitlement is enforced. properties: consent_banner: - $ref: "#/components/schemas/ConsentBannerWrite" + $ref: "#/components/schemas/ConsentBannerCreate" required: - consent_banner From 8ebe8dd9eb32572557194106dd5056eefcbea8da Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 18:03:44 +0000 Subject: [PATCH 16/19] Document onboarding_status as unknowable during an outage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status is derived from the visitors database, which can be down while the account itself reads and writes fine. The API now answers null rather than failing the response or guessing a status from the branches it can still evaluate — say so, and say that null is not a downgrade. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012BAKTdSBroe771g8CYkGJp --- openapi.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openapi.yaml b/openapi.yaml index 2425e85..af5a878 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1778,6 +1778,10 @@ components: - `code_installed`: The script has been installed on the user's website. - `inactive`: Less than 10 visitors to the user's website in the last 30 days. - `live`: The account is live and operational. + + Accounts whose status could not be determined are excluded from a + filtered listing, since their status is unknown rather than any + particular value. required: false schema: type: string @@ -2925,7 +2929,7 @@ components: readOnly: true description: ID of the account user that owns the account onboarding_status: - type: string + type: [string, "null"] readOnly: true description: | The status of the account, where: @@ -2934,6 +2938,11 @@ components: - `code_installed`: The script has been installed on the user's website. - `inactive`: Less than 10 visitors to the user's website in the last 30 days. - `live`: The account is live and operational. + + `null` means the status could not be determined — the analytics store + behind it was unreachable when the response was built. It is not a + statement about the account, and the request itself still succeeded. + Treat it as "unknown" and retry rather than as a downgrade in status. visitor_stats: $ref: "#/components/schemas/VisitorStats" created_at: From f888c9f1dc1276e35b4f95157a1ec3bf0d3b168c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 18:26:51 +0000 Subject: [PATCH 17/19] Document service translations, and stop advertising a field create rejects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The services endpoints accept and return localized copy now, so describe the nested attributes and the translations the response carries. ConsentBannerCreate removed `enabled` with `not: {}`, which does not remove a property — tooling that flattens allOf still advertised it, and strictly it made any payload carrying `enabled` invalid when the API in fact accepts it and ignores the value. The settings are their own schema now, `enabled` composes on top for update and read, and create refers to the settings alone. --- openapi.yaml | 83 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index af5a878..e2161f9 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3078,6 +3078,36 @@ components: description: | Domains to auto-block, separated by commas or newlines. Parsed into the `auto_block_domains` list on save. + site_service_translations_attributes: + type: array + description: | + Create, update or discard the localized copy a visitor reads when the + banner runs in another language. Send an entry without an `id` to add + a translation, with the `id` the response gave you to change one, and + with `discarded_at` set to remove one. Unlike banner translations + there is no plan entitlement behind these, so they can be supplied + when creating the service. + items: + type: object + properties: + id: + type: string + description: | + The translation's ID, as returned in `translations`. Omit it to + add a new translation. + language_code: + type: string + description: | + The language this copy is for. Cannot be the service's own + `default_language`, and each language may appear once. + description: + type: [string, "null"] + additional_info: + type: [string, "null"] + discarded_at: + type: [string, "null"] + format: date-time + description: Set to remove this translation SiteServiceCreate: description: | @@ -3156,6 +3186,23 @@ components: type: [string, "null"] name: type: [string, "null"] + translations: + type: array + description: | + The localized copy for this service, one entry per language, with the + IDs `site_service_translations_attributes` expects back. Discarded + translations are not returned. + items: + type: object + properties: + id: + type: string + language_code: + type: string + description: + type: [string, "null"] + additional_info: + type: [string, "null"] created_at: type: string format: date-time @@ -3179,6 +3226,11 @@ components: category: id: "cat1" name: "Analytics" + translations: + - id: "svct1" + language_code: "de" + description: "Misst, wie Besucher die Website nutzen." + additional_info: null created_at: "2023-03-01T09:00:00Z" updated_at: "2023-03-23T12:34:56Z" @@ -3188,7 +3240,7 @@ components: data: $ref: "#/components/schemas/SiteService" - ConsentBannerWrite: + ConsentBannerSettings: type: object description: | Every setting the banner designer exposes. The response returns the same @@ -3238,9 +3290,6 @@ components: enable_tcf: type: boolean description: Whether the banner runs in IAB TCF mode - enabled: - type: boolean - description: Whether the banner is live. New banners are always created disabled. equal_weight_buttons: type: boolean floating_preferences_icon_position: @@ -3324,6 +3373,19 @@ components: type: string description: Colour overrides, keyed by button element + ConsentBannerWrite: + description: | + Every setting the banner designer exposes, plus whether the banner is + live. Creating a banner uses ConsentBannerSettings instead, which is this + same set without `enabled`. + allOf: + - $ref: "#/components/schemas/ConsentBannerSettings" + - type: object + properties: + enabled: + type: boolean + description: Whether the banner is live. + ConsentBannerUpdate: description: | Everything a banner accepts on update, including its translations. @@ -3391,15 +3453,12 @@ components: ConsentBannerCreate: description: | - The create payload. `enabled` is absent because a new banner is always - created disabled — enable it with an update once its content and - targeting are set. + The create payload: every banner setting except `enabled`, which is not + part of it because a new banner is always created disabled — enable it + with an update once its content and targeting are set. Sending `enabled` + anyway is not an error, the value is simply ignored. allOf: - - $ref: "#/components/schemas/ConsentBannerWrite" - - type: object - properties: - enabled: - not: {} + - $ref: "#/components/schemas/ConsentBannerSettings" ConsentBannerCreateRequest: type: object From 6f408e8728f7cd44ee1bf29f01d02380f9f93a7a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 18:45:51 +0000 Subject: [PATCH 18/19] Say which translation fields a new entry needs, and drop them from create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A nested entry without an id creates a translation, and cannot be created without naming its language — so require one of id or language_code rather than leaving both optional, on service translations and banner translations alike. Service translations are no longer accepted when creating a service, for the same reason banner translations are not: the language entitlement is enforced on update. Split out of the create schema the way `enabled` was, so create does not advertise a field it ignores. --- openapi.yaml | 90 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 32 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index e2161f9..f5513c8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3043,7 +3043,7 @@ components: support_email: "privacy@example.com" banner_debug_mode: false - SiteServiceWrite: + SiteServiceSettings: type: object properties: name: @@ -3078,43 +3078,62 @@ components: description: | Domains to auto-block, separated by commas or newlines. Parsed into the `auto_block_domains` list on save. - site_service_translations_attributes: - type: array - description: | - Create, update or discard the localized copy a visitor reads when the - banner runs in another language. Send an entry without an `id` to add - a translation, with the `id` the response gave you to change one, and - with `discarded_at` set to remove one. Unlike banner translations - there is no plan entitlement behind these, so they can be supplied - when creating the service. - items: - type: object - properties: - id: - type: string - description: | - The translation's ID, as returned in `translations`. Omit it to - add a new translation. - language_code: - type: string + + SiteServiceWrite: + description: | + Everything a service accepts on update, including its translations. + allOf: + - $ref: "#/components/schemas/SiteServiceSettings" + - type: object + properties: + site_service_translations_attributes: + type: array + description: | + Create, update or discard the localized copy a visitor reads when the + banner runs in another language. Send an entry without an `id` to add + a translation, with the `id` the response gave you to change one, and + with `discarded_at` set to remove one. + + Not accepted when creating a service — add them by updating it, which + is where the account's language entitlement is enforced. + items: + type: object description: | - The language this copy is for. Cannot be the service's own - `default_language`, and each language may appear once. - description: - type: [string, "null"] - additional_info: - type: [string, "null"] - discarded_at: - type: [string, "null"] - format: date-time - description: Set to remove this translation + An entry without an `id` creates a translation and must name its + `language_code`; an entry with one may send only the fields it + changes. + anyOf: + - required: [id] + - required: [language_code] + properties: + id: + type: string + description: | + The translation's ID, as returned in `translations`. Omit it to + add a new translation. + language_code: + type: string + description: | + The language this copy is for. Cannot be the service's own + `default_language`, and each language may appear once. + description: + type: [string, "null"] + additional_info: + type: [string, "null"] + discarded_at: + type: [string, "null"] + format: date-time + description: Set to remove this translation SiteServiceCreate: description: | A service is always filed under a category, so `category_id` is - required here even though it is optional when updating. + required here even though it is optional when updating. Translations are + absent for the same reason they are on banners — add them by updating + the service, which is where the account's language entitlement is + enforced. Sending them anyway is not an error, they are simply ignored. allOf: - - $ref: "#/components/schemas/SiteServiceWrite" + - $ref: "#/components/schemas/SiteServiceSettings" required: - name - category_id @@ -3405,6 +3424,13 @@ components: one, and set `discarded_at` to remove it. additionalProperties: type: object + description: | + An entry without an `id` creates a translation and must name its + `language_code`; an entry with one may send only the fields it + changes. + anyOf: + - required: [id] + - required: [language_code] properties: id: type: string From f02e0475cf63772e17bb9af1861cfa72e8455728 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 18:55:51 +0000 Subject: [PATCH 19/19] Return targeting where update accepts it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The response nested target_type and the region arrays under a `targeting` object while the update endpoint takes them at the top level, so the one part of a banner a client could not read and send back unchanged was its targeting. They are flat now, on both sides — the read schema already inherits them from the settings it composes. --- openapi.yaml | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/openapi.yaml b/openapi.yaml index f5513c8..216f59a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3550,26 +3550,6 @@ components: type: integer readOnly: true description: Bumped when a change requires visitors to consent again - targeting: - type: object - properties: - target_type: - type: string - target_countries: - type: array - items: - type: string - target_us_states: - type: array - items: - type: string - target_canadian_regions: - type: array - items: - type: string - description: | - Populated instead of `target_us_states` when `target_type` is - `selected_canadian_provinces`; the two share a column. translations: type: array readOnly: true @@ -3645,10 +3625,10 @@ components: dark_mode: "light" language_autodetect: "document" revision: 2 - targeting: - target_type: "selected_countries" - target_countries: ["DE", "FR"] - target_us_states: [] + target_type: "selected_countries" + target_countries: ["DE", "FR"] + target_us_states: [] + target_canadian_regions: [] translations: - id: "trn1" language_code: "de"