From ff63e360c73329d1806bace3a0f8107bc2c7206e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Mar 2026 15:23:09 -0700 Subject: [PATCH 1/4] feat(ketch): add Ketch privacy consent integration --- apps/docs/components/icons.tsx | 12 + apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/ketch.mdx | 149 ++++++++++++ apps/docs/content/docs/en/tools/meta.json | 1 + .../integrations/data/icon-mapping.ts | 2 + .../integrations/data/integrations.json | 39 ++++ apps/sim/blocks/blocks/ketch.ts | 220 ++++++++++++++++++ apps/sim/blocks/registry.ts | 2 + apps/sim/components/icons.tsx | 12 + apps/sim/tools/ketch/get_consent.ts | 95 ++++++++ apps/sim/tools/ketch/get_subscriptions.ts | 83 +++++++ apps/sim/tools/ketch/index.ts | 11 + apps/sim/tools/ketch/invoke_right.ts | 123 ++++++++++ apps/sim/tools/ketch/set_consent.ts | 106 +++++++++ apps/sim/tools/ketch/set_subscriptions.ts | 109 +++++++++ apps/sim/tools/ketch/types.ts | 117 ++++++++++ apps/sim/tools/registry.ts | 12 + 17 files changed, 1095 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/ketch.mdx create mode 100644 apps/sim/blocks/blocks/ketch.ts create mode 100644 apps/sim/tools/ketch/get_consent.ts create mode 100644 apps/sim/tools/ketch/get_subscriptions.ts create mode 100644 apps/sim/tools/ketch/index.ts create mode 100644 apps/sim/tools/ketch/invoke_right.ts create mode 100644 apps/sim/tools/ketch/set_consent.ts create mode 100644 apps/sim/tools/ketch/set_subscriptions.ts create mode 100644 apps/sim/tools/ketch/types.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 0cf5773ca48..27d1a2a974c 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4931,6 +4931,18 @@ export function KalshiIcon(props: SVGProps) { ) } +export function KetchIcon(props: SVGProps) { + return ( + + + + + ) +} + export function PolymarketIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index 900229eaad7..8cd468289a9 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -88,6 +88,7 @@ import { JiraIcon, JiraServiceManagementIcon, KalshiIcon, + KetchIcon, LangsmithIcon, LemlistIcon, LinearIcon, @@ -262,6 +263,7 @@ export const blockTypeToIconMap: Record = { jira: JiraIcon, jira_service_management: JiraServiceManagementIcon, kalshi_v2: KalshiIcon, + ketch: KetchIcon, knowledge: PackageSearchIcon, langsmith: LangsmithIcon, lemlist: LemlistIcon, diff --git a/apps/docs/content/docs/en/tools/ketch.mdx b/apps/docs/content/docs/en/tools/ketch.mdx new file mode 100644 index 00000000000..145fec7ddbb --- /dev/null +++ b/apps/docs/content/docs/en/tools/ketch.mdx @@ -0,0 +1,149 @@ +--- +title: Ketch +description: Manage privacy consent, subscriptions, and data subject rights +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Ketch](https://www.ketch.com/) is an AI-powered privacy, consent, and data governance platform that helps organizations automate compliance with global privacy regulations. It provides tools for managing consent preferences, handling data subject rights requests, and controlling subscription communications. + +With Ketch, you can: + +- **Retrieve consent status**: Query the current consent preferences for any data subject across configured purposes and legal bases +- **Update consent preferences**: Set or modify consent for specific purposes (e.g., analytics, marketing) with the appropriate legal basis (opt-in, opt-out, disclosure) +- **Manage subscriptions**: Get and update subscription topic preferences and global controls across contact methods like email and SMS +- **Invoke data subject rights**: Submit privacy rights requests including data access, deletion, correction, and processing restriction under regulations like GDPR and CCPA + +To use Ketch, drop the Ketch block into your workflow and provide your organization code, property code, and environment code. The Ketch Web API is a public API — no API key or OAuth credentials are required. Identity is determined by the organization and property codes along with the data subject's identity (e.g., email address). + +These capabilities let you automate privacy compliance workflows, respond to user consent changes in real time, and manage data subject rights requests as part of your broader automation pipelines. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate Ketch into the workflow. Retrieve and update consent preferences, manage subscription topics and controls, and submit data subject rights requests for access, deletion, correction, or processing restriction. + + + +## Tools + +### `ketch_get_consent` + +Retrieve consent status for a data subject. Returns the current consent preferences for each configured purpose. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organizationCode` | string | Yes | Ketch organization code | +| `propertyCode` | string | Yes | Digital property code defined in Ketch | +| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) | +| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) | +| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) | +| `purposes` | json | No | Optional purposes to filter the consent query | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `purposes` | object | Map of purpose codes to consent status and legal basis | +| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" | +| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) | +| `vendors` | object | Map of vendor consent statuses | + +### `ketch_set_consent` + +Update consent preferences for a data subject. Sets the consent status for specified purposes with the appropriate legal basis. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organizationCode` | string | Yes | Ketch organization code | +| `propertyCode` | string | Yes | Digital property code defined in Ketch | +| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) | +| `jurisdictionCode` | string | No | Jurisdiction code \(e.g., "gdpr", "ccpa"\) | +| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) | +| `purposes` | json | Yes | Map of purpose codes to consent settings \(e.g., \{"analytics": \{"allowed": "granted", "legalBasisCode": "consent_optin"\}\}\) | +| `collectedAt` | number | No | UNIX timestamp when consent was collected \(defaults to current time\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `purposes` | object | Updated consent status map of purpose codes to consent settings | +| ↳ `allowed` | string | Consent status for the purpose: "granted" or "denied" | +| ↳ `legalBasisCode` | string | Legal basis code \(e.g., "consent_optin", "consent_optout", "disclosure", "other"\) | + +### `ketch_get_subscriptions` + +Retrieve subscription preferences for a data subject. Returns the current subscription topic and control statuses. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organizationCode` | string | Yes | Ketch organization code | +| `propertyCode` | string | Yes | Digital property code defined in Ketch | +| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) | +| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `topics` | object | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}\}\}\) | +| `controls` | object | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) | + +### `ketch_set_subscriptions` + +Update subscription preferences for a data subject. Sets topic and control statuses for email, SMS, and other contact methods. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organizationCode` | string | Yes | Ketch organization code | +| `propertyCode` | string | Yes | Digital property code defined in Ketch | +| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) | +| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) | +| `topics` | json | No | Map of topic codes to contact method settings \(e.g., \{"newsletter": \{"email": \{"status": "granted"\}, "sms": \{"status": "denied"\}\}\}\) | +| `controls` | json | No | Map of control codes to settings \(e.g., \{"global_unsubscribe": \{"status": "denied"\}\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the subscription preferences were updated | + +### `ketch_invoke_right` + +Submit a data subject rights request (e.g., access, delete, correct, restrict processing). Initiates a privacy rights workflow in Ketch. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `organizationCode` | string | Yes | Ketch organization code | +| `propertyCode` | string | Yes | Digital property code defined in Ketch | +| `environmentCode` | string | Yes | Environment code defined in Ketch \(e.g., "production"\) | +| `jurisdictionCode` | string | Yes | Jurisdiction code \(e.g., "gdpr", "ccpa"\) | +| `rightCode` | string | Yes | Privacy right code to invoke \(e.g., "access", "delete", "correct", "restrict_processing"\) | +| `identities` | json | Yes | Identity map \(e.g., \{"email": "user@example.com"\}\) | +| `userData` | json | No | Optional data subject information \(e.g., \{"email": "user@example.com", "firstName": "John", "lastName": "Doe"\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the rights request was submitted | +| `message` | string | Response message from Ketch | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 9ff8f3ff78a..cd1c2976b44 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -83,6 +83,7 @@ "jira", "jira_service_management", "kalshi", + "ketch", "knowledge", "langsmith", "lemlist", diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 7580b713c57..31c1770175a 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -88,6 +88,7 @@ import { JiraIcon, JiraServiceManagementIcon, KalshiIcon, + KetchIcon, LangsmithIcon, LemlistIcon, LinearIcon, @@ -262,6 +263,7 @@ export const blockTypeToIconMap: Record = { jira: JiraIcon, jira_service_management: JiraServiceManagementIcon, kalshi_v2: KalshiIcon, + ketch: KetchIcon, knowledge: PackageSearchIcon, langsmith: LangsmithIcon, lemlist: LemlistIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 7d8cf706be5..de0e30384c6 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -6121,6 +6121,45 @@ "integrationType": "analytics", "tags": ["prediction-markets", "data-analytics"] }, + { + "type": "ketch", + "slug": "ketch", + "name": "Ketch", + "description": "Manage privacy consent, subscriptions, and data subject rights", + "longDescription": "Integrate Ketch into the workflow. Retrieve and update consent preferences, manage subscription topics and controls, and submit data subject rights requests for access, deletion, correction, or processing restriction.", + "bgColor": "#9B5CFF", + "iconName": "KetchIcon", + "docsUrl": "https://docs.sim.ai/tools/ketch", + "operations": [ + { + "name": "Get Consent", + "description": "Retrieve consent status for a data subject. Returns the current consent preferences for each configured purpose." + }, + { + "name": "Set Consent", + "description": "Update consent preferences for a data subject. Sets the consent status for specified purposes with the appropriate legal basis." + }, + { + "name": "Get Subscriptions", + "description": "Retrieve subscription preferences for a data subject. Returns the current subscription topic and control statuses." + }, + { + "name": "Set Subscriptions", + "description": "Update subscription preferences for a data subject. Sets topic and control statuses for email, SMS, and other contact methods." + }, + { + "name": "Invoke Right", + "description": "Submit a data subject rights request (e.g., access, delete, correct, restrict processing). Initiates a privacy rights workflow in Ketch." + } + ], + "operationCount": 5, + "triggers": [], + "triggerCount": 0, + "authType": "none", + "category": "tools", + "integrationType": "security", + "tags": ["identity"] + }, { "type": "knowledge", "slug": "knowledge", diff --git a/apps/sim/blocks/blocks/ketch.ts b/apps/sim/blocks/blocks/ketch.ts new file mode 100644 index 00000000000..8b359ed0086 --- /dev/null +++ b/apps/sim/blocks/blocks/ketch.ts @@ -0,0 +1,220 @@ +import { KetchIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { IntegrationType } from '@/blocks/types' +import type { KetchResponse } from '@/tools/ketch/types' + +export const KetchBlock: BlockConfig = { + type: 'ketch', + name: 'Ketch', + description: 'Manage privacy consent, subscriptions, and data subject rights', + longDescription: + 'Integrate Ketch into the workflow. Retrieve and update consent preferences, manage subscription topics and controls, and submit data subject rights requests for access, deletion, correction, or processing restriction.', + docsLink: 'https://docs.sim.ai/tools/ketch', + category: 'tools', + integrationType: IntegrationType.Security, + tags: ['identity'], + bgColor: '#9B5CFF', + icon: KetchIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Get Consent', id: 'get_consent' }, + { label: 'Set Consent', id: 'set_consent' }, + { label: 'Get Subscriptions', id: 'get_subscriptions' }, + { label: 'Set Subscriptions', id: 'set_subscriptions' }, + { label: 'Invoke Right', id: 'invoke_right' }, + ], + value: () => 'get_consent', + }, + { + id: 'organizationCode', + title: 'Organization Code', + type: 'short-input', + placeholder: 'Enter your Ketch organization code', + required: true, + }, + { + id: 'propertyCode', + title: 'Property Code', + type: 'short-input', + placeholder: 'Enter the digital property code', + required: true, + }, + { + id: 'environmentCode', + title: 'Environment Code', + type: 'short-input', + placeholder: 'e.g., production', + required: true, + }, + { + id: 'jurisdictionCode', + title: 'Jurisdiction Code', + type: 'short-input', + placeholder: 'e.g., gdpr, ccpa', + condition: { field: 'operation', value: 'invoke_right' }, + required: { field: 'operation', value: 'invoke_right' }, + }, + { + id: 'jurisdictionCodeOptional', + title: 'Jurisdiction Code', + type: 'short-input', + placeholder: 'e.g., gdpr, ccpa (optional)', + condition: { field: 'operation', value: ['get_consent', 'set_consent'] }, + mode: 'advanced', + }, + { + id: 'identities', + title: 'Identities', + type: 'code', + placeholder: '{"email": "user@example.com"}', + language: 'json', + required: true, + }, + { + id: 'purposes', + title: 'Purposes', + type: 'code', + placeholder: + '{"analytics": {"allowed": "granted", "legalBasisCode": "consent_optin"}, "marketing": {"allowed": "denied"}}', + language: 'json', + condition: { field: 'operation', value: 'set_consent' }, + required: { field: 'operation', value: 'set_consent' }, + }, + { + id: 'topics', + title: 'Subscription Topics', + type: 'code', + placeholder: '{"newsletter": {"email": {"status": "granted"}, "sms": {"status": "denied"}}}', + language: 'json', + condition: { field: 'operation', value: 'set_subscriptions' }, + }, + { + id: 'controls', + title: 'Subscription Controls', + type: 'code', + placeholder: '{"global_unsubscribe": {"status": "denied"}}', + language: 'json', + condition: { field: 'operation', value: 'set_subscriptions' }, + }, + { + id: 'rightCode', + title: 'Right Code', + type: 'dropdown', + options: [ + { label: 'Access', id: 'access' }, + { label: 'Delete', id: 'delete' }, + { label: 'Correct', id: 'correct' }, + { label: 'Restrict Processing', id: 'restrict_processing' }, + ], + condition: { field: 'operation', value: 'invoke_right' }, + required: { field: 'operation', value: 'invoke_right' }, + }, + { + id: 'userData', + title: 'User Data', + type: 'code', + placeholder: '{"email": "user@example.com", "firstName": "John", "lastName": "Doe"}', + language: 'json', + condition: { field: 'operation', value: 'invoke_right' }, + mode: 'advanced', + }, + { + id: 'collectedAt', + title: 'Collected At (UNIX timestamp)', + type: 'short-input', + placeholder: 'Defaults to current time', + condition: { field: 'operation', value: 'set_consent' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a UNIX timestamp in seconds for the current time. Return ONLY the numeric timestamp string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + ], + tools: { + access: [ + 'ketch_get_consent', + 'ketch_set_consent', + 'ketch_get_subscriptions', + 'ketch_set_subscriptions', + 'ketch_invoke_right', + ], + config: { + tool: (params) => `ketch_${params.operation}`, + params: (params) => { + const result: Record = { + organizationCode: params.organizationCode, + propertyCode: params.propertyCode, + environmentCode: params.environmentCode, + } + + const jurisdictionCode = params.jurisdictionCode || params.jurisdictionCodeOptional + if (jurisdictionCode) result.jurisdictionCode = jurisdictionCode + + if (params.identities) { + result.identities = + typeof params.identities === 'string' + ? JSON.parse(params.identities) + : params.identities + } + + if (params.operation === 'set_consent' && params.purposes) { + result.purposes = + typeof params.purposes === 'string' ? JSON.parse(params.purposes) : params.purposes + if (params.collectedAt) result.collectedAt = Number(params.collectedAt) + } + + if (params.operation === 'set_subscriptions') { + if (params.topics) { + result.topics = + typeof params.topics === 'string' ? JSON.parse(params.topics) : params.topics + } + if (params.controls) { + result.controls = + typeof params.controls === 'string' ? JSON.parse(params.controls) : params.controls + } + } + + if (params.operation === 'invoke_right') { + if (params.rightCode) result.rightCode = params.rightCode + if (params.userData) { + result.userData = + typeof params.userData === 'string' ? JSON.parse(params.userData) : params.userData + } + } + + return result + }, + }, + }, + inputs: { + organizationCode: { type: 'string', description: 'Ketch organization code' }, + propertyCode: { type: 'string', description: 'Digital property code' }, + environmentCode: { type: 'string', description: 'Environment code' }, + jurisdictionCode: { type: 'string', description: 'Jurisdiction code' }, + identities: { type: 'json', description: 'Identity map for the data subject' }, + purposes: { type: 'json', description: 'Consent purposes map' }, + topics: { type: 'json', description: 'Subscription topics map' }, + controls: { type: 'json', description: 'Subscription controls map' }, + rightCode: { type: 'string', description: 'Privacy right code' }, + userData: { type: 'json', description: 'Data subject information' }, + collectedAt: { type: 'number', description: 'UNIX timestamp of consent collection' }, + }, + outputs: { + purposes: { type: 'json', description: 'Consent status per purpose (allowed, legalBasisCode)' }, + vendors: { type: 'json', description: 'Vendor consent statuses' }, + topics: { + type: 'json', + description: 'Subscription topic statuses per contact method', + }, + controls: { type: 'json', description: 'Subscription control statuses' }, + success: { type: 'boolean', description: 'Whether the request succeeded' }, + message: { type: 'string', description: 'Response message from Ketch' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 15363a8ad53..a01422e20c3 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -91,6 +91,7 @@ import { JinaBlock } from '@/blocks/blocks/jina' import { JiraBlock } from '@/blocks/blocks/jira' import { JiraServiceManagementBlock } from '@/blocks/blocks/jira_service_management' import { KalshiBlock, KalshiV2Block } from '@/blocks/blocks/kalshi' +import { KetchBlock } from '@/blocks/blocks/ketch' import { KnowledgeBlock } from '@/blocks/blocks/knowledge' import { LangsmithBlock } from '@/blocks/blocks/langsmith' import { LemlistBlock } from '@/blocks/blocks/lemlist' @@ -312,6 +313,7 @@ export const registry: Record = { jira_service_management: JiraServiceManagementBlock, kalshi: KalshiBlock, kalshi_v2: KalshiV2Block, + ketch: KetchBlock, knowledge: KnowledgeBlock, langsmith: LangsmithBlock, lemlist: LemlistBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 0cf5773ca48..27d1a2a974c 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4931,6 +4931,18 @@ export function KalshiIcon(props: SVGProps) { ) } +export function KetchIcon(props: SVGProps) { + return ( + + + + + ) +} + export function PolymarketIcon(props: SVGProps) { return ( diff --git a/apps/sim/tools/ketch/get_consent.ts b/apps/sim/tools/ketch/get_consent.ts new file mode 100644 index 00000000000..75cfc0d4d12 --- /dev/null +++ b/apps/sim/tools/ketch/get_consent.ts @@ -0,0 +1,95 @@ +import type { KetchGetConsentParams, KetchGetConsentResponse } from '@/tools/ketch/types' +import { CONSENT_PURPOSE_OUTPUT_PROPERTIES } from '@/tools/ketch/types' +import type { ToolConfig } from '@/tools/types' + +export const getConsentTool: ToolConfig = { + id: 'ketch_get_consent', + name: 'Ketch Get Consent', + description: + 'Retrieve consent status for a data subject. Returns the current consent preferences for each configured purpose.', + version: '1.0.0', + + params: { + organizationCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Ketch organization code', + }, + propertyCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Digital property code defined in Ketch', + }, + environmentCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Environment code defined in Ketch (e.g., "production")', + }, + jurisdictionCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Jurisdiction code (e.g., "gdpr", "ccpa")', + }, + identities: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Identity map (e.g., {"email": "user@example.com"})', + }, + purposes: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: 'Optional purposes to filter the consent query', + }, + }, + + request: { + url: (params) => + `https://global.ketchcdn.com/web/v2/consent/${encodeURIComponent(params.organizationCode.trim())}/get`, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + organizationCode: params.organizationCode.trim(), + propertyCode: params.propertyCode, + environmentCode: params.environmentCode, + identities: params.identities, + } + if (params.jurisdictionCode) body.jurisdictionCode = params.jurisdictionCode + if (params.purposes) body.purposes = params.purposes + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + purposes: data.purposes ?? {}, + vendors: data.vendors ?? null, + }, + } + }, + + outputs: { + purposes: { + type: 'object', + description: 'Map of purpose codes to consent status and legal basis', + properties: CONSENT_PURPOSE_OUTPUT_PROPERTIES, + }, + vendors: { + type: 'object', + description: 'Map of vendor consent statuses', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/ketch/get_subscriptions.ts b/apps/sim/tools/ketch/get_subscriptions.ts new file mode 100644 index 00000000000..a02cc598a45 --- /dev/null +++ b/apps/sim/tools/ketch/get_subscriptions.ts @@ -0,0 +1,83 @@ +import type { + KetchGetSubscriptionsParams, + KetchGetSubscriptionsResponse, +} from '@/tools/ketch/types' +import type { ToolConfig } from '@/tools/types' + +export const getSubscriptionsTool: ToolConfig< + KetchGetSubscriptionsParams, + KetchGetSubscriptionsResponse +> = { + id: 'ketch_get_subscriptions', + name: 'Ketch Get Subscriptions', + description: + 'Retrieve subscription preferences for a data subject. Returns the current subscription topic and control statuses.', + version: '1.0.0', + + params: { + organizationCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Ketch organization code', + }, + propertyCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Digital property code defined in Ketch', + }, + environmentCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Environment code defined in Ketch (e.g., "production")', + }, + identities: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Identity map (e.g., {"email": "user@example.com"})', + }, + }, + + request: { + url: (params) => + `https://global.ketchcdn.com/web/v2/subscriptions/${encodeURIComponent(params.organizationCode.trim())}/get`, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => ({ + organizationCode: params.organizationCode.trim(), + propertyCode: params.propertyCode, + environmentCode: params.environmentCode, + identities: params.identities, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + topics: data.topics ?? {}, + controls: data.controls ?? {}, + }, + } + }, + + outputs: { + topics: { + type: 'object', + description: + 'Map of topic codes to contact method settings (e.g., {"newsletter": {"email": {"status": "granted"}}})', + }, + controls: { + type: 'object', + description: + 'Map of control codes to settings (e.g., {"global_unsubscribe": {"status": "denied"}})', + }, + }, +} diff --git a/apps/sim/tools/ketch/index.ts b/apps/sim/tools/ketch/index.ts new file mode 100644 index 00000000000..49194f9678e --- /dev/null +++ b/apps/sim/tools/ketch/index.ts @@ -0,0 +1,11 @@ +import { getConsentTool } from '@/tools/ketch/get_consent' +import { getSubscriptionsTool } from '@/tools/ketch/get_subscriptions' +import { invokeRightTool } from '@/tools/ketch/invoke_right' +import { setConsentTool } from '@/tools/ketch/set_consent' +import { setSubscriptionsTool } from '@/tools/ketch/set_subscriptions' + +export const ketchGetConsentTool = getConsentTool +export const ketchSetConsentTool = setConsentTool +export const ketchInvokeRightTool = invokeRightTool +export const ketchGetSubscriptionsTool = getSubscriptionsTool +export const ketchSetSubscriptionsTool = setSubscriptionsTool diff --git a/apps/sim/tools/ketch/invoke_right.ts b/apps/sim/tools/ketch/invoke_right.ts new file mode 100644 index 00000000000..1b9403a0bb9 --- /dev/null +++ b/apps/sim/tools/ketch/invoke_right.ts @@ -0,0 +1,123 @@ +import type { KetchInvokeRightParams, KetchInvokeRightResponse } from '@/tools/ketch/types' +import type { ToolConfig } from '@/tools/types' + +export const invokeRightTool: ToolConfig = { + id: 'ketch_invoke_right', + name: 'Ketch Invoke Right', + description: + 'Submit a data subject rights request (e.g., access, delete, correct, restrict processing). Initiates a privacy rights workflow in Ketch.', + version: '1.0.0', + + params: { + organizationCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Ketch organization code', + }, + propertyCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Digital property code defined in Ketch', + }, + environmentCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Environment code defined in Ketch (e.g., "production")', + }, + jurisdictionCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Jurisdiction code (e.g., "gdpr", "ccpa")', + }, + rightCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'Privacy right code to invoke (e.g., "access", "delete", "correct", "restrict_processing")', + }, + identities: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Identity map (e.g., {"email": "user@example.com"})', + }, + userData: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Optional data subject information (e.g., {"email": "user@example.com", "firstName": "John", "lastName": "Doe"})', + }, + }, + + request: { + url: (params) => + `https://global.ketchcdn.com/web/v2/rights/${encodeURIComponent(params.organizationCode.trim())}/invoke`, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + organizationCode: params.organizationCode.trim(), + propertyCode: params.propertyCode, + environmentCode: params.environmentCode, + jurisdictionCode: params.jurisdictionCode, + rightCode: params.rightCode, + identities: params.identities, + } + if (params.userData) body.user = params.userData + return body + }, + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + let errorMessage = `Request failed with status ${response.status}` + try { + const data = await response.json() + errorMessage = data.message ?? data.error ?? errorMessage + } catch { + // No JSON body in error response + } + return { + success: false, + output: { + success: false, + message: errorMessage, + }, + } + } + + let message: string | null = null + try { + const data = await response.json() + message = data.message ?? null + } catch { + // 204 No Content - no body to parse + } + + return { + success: true, + output: { + success: true, + message, + }, + } + }, + + outputs: { + success: { type: 'boolean', description: 'Whether the rights request was submitted' }, + message: { + type: 'string', + description: 'Response message from Ketch', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/ketch/set_consent.ts b/apps/sim/tools/ketch/set_consent.ts new file mode 100644 index 00000000000..55a0c973427 --- /dev/null +++ b/apps/sim/tools/ketch/set_consent.ts @@ -0,0 +1,106 @@ +import type { KetchSetConsentParams, KetchSetConsentResponse } from '@/tools/ketch/types' +import { CONSENT_PURPOSE_OUTPUT_PROPERTIES } from '@/tools/ketch/types' +import type { ToolConfig } from '@/tools/types' + +export const setConsentTool: ToolConfig = { + id: 'ketch_set_consent', + name: 'Ketch Set Consent', + description: + 'Update consent preferences for a data subject. Sets the consent status for specified purposes with the appropriate legal basis.', + version: '1.0.0', + + params: { + organizationCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Ketch organization code', + }, + propertyCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Digital property code defined in Ketch', + }, + environmentCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Environment code defined in Ketch (e.g., "production")', + }, + jurisdictionCode: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Jurisdiction code (e.g., "gdpr", "ccpa")', + }, + identities: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Identity map (e.g., {"email": "user@example.com"})', + }, + purposes: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Map of purpose codes to consent settings (e.g., {"analytics": {"allowed": "granted", "legalBasisCode": "consent_optin"}})', + }, + collectedAt: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'UNIX timestamp when consent was collected (defaults to current time)', + }, + }, + + request: { + url: (params) => + `https://global.ketchcdn.com/web/v2/consent/${encodeURIComponent(params.organizationCode.trim())}/update`, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + organizationCode: params.organizationCode.trim(), + propertyCode: params.propertyCode, + environmentCode: params.environmentCode, + identities: params.identities, + purposes: params.purposes, + collectedAt: params.collectedAt ?? Math.floor(Date.now() / 1000), + } + if (params.jurisdictionCode) body.jurisdictionCode = params.jurisdictionCode + return body + }, + }, + + transformResponse: async (response: Response) => { + if (response.status === 204) { + return { + success: true, + output: { + purposes: {}, + }, + } + } + + const data = await response.json() + return { + success: true, + output: { + purposes: data.purposes ?? {}, + }, + } + }, + + outputs: { + purposes: { + type: 'object', + description: 'Updated consent status map of purpose codes to consent settings', + properties: CONSENT_PURPOSE_OUTPUT_PROPERTIES, + }, + }, +} diff --git a/apps/sim/tools/ketch/set_subscriptions.ts b/apps/sim/tools/ketch/set_subscriptions.ts new file mode 100644 index 00000000000..a71d18015fd --- /dev/null +++ b/apps/sim/tools/ketch/set_subscriptions.ts @@ -0,0 +1,109 @@ +import type { + KetchSetSubscriptionsParams, + KetchSetSubscriptionsResponse, +} from '@/tools/ketch/types' +import type { ToolConfig } from '@/tools/types' + +export const setSubscriptionsTool: ToolConfig< + KetchSetSubscriptionsParams, + KetchSetSubscriptionsResponse +> = { + id: 'ketch_set_subscriptions', + name: 'Ketch Set Subscriptions', + description: + 'Update subscription preferences for a data subject. Sets topic and control statuses for email, SMS, and other contact methods.', + version: '1.0.0', + + params: { + organizationCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Ketch organization code', + }, + propertyCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Digital property code defined in Ketch', + }, + environmentCode: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Environment code defined in Ketch (e.g., "production")', + }, + identities: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: 'Identity map (e.g., {"email": "user@example.com"})', + }, + topics: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Map of topic codes to contact method settings (e.g., {"newsletter": {"email": {"status": "granted"}, "sms": {"status": "denied"}}})', + }, + controls: { + type: 'json', + required: false, + visibility: 'user-or-llm', + description: + 'Map of control codes to settings (e.g., {"global_unsubscribe": {"status": "denied"}})', + }, + }, + + request: { + url: (params) => + `https://global.ketchcdn.com/web/v2/subscriptions/${encodeURIComponent(params.organizationCode.trim())}/update`, + method: 'POST', + headers: () => ({ + Accept: 'application/json', + 'Content-Type': 'application/json', + }), + body: (params) => { + const body: Record = { + organizationCode: params.organizationCode.trim(), + propertyCode: params.propertyCode, + environmentCode: params.environmentCode, + identities: params.identities, + } + if (params.topics) body.topics = params.topics + if (params.controls) body.controls = params.controls + return body + }, + }, + + transformResponse: async (response: Response) => { + if (response.status === 204) { + return { + success: true, + output: { + success: true, + }, + } + } + + try { + await response.json() + } catch { + // 204-like response with no body + } + + return { + success: true, + output: { + success: true, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the subscription preferences were updated', + }, + }, +} diff --git a/apps/sim/tools/ketch/types.ts b/apps/sim/tools/ketch/types.ts new file mode 100644 index 00000000000..c231b0b9097 --- /dev/null +++ b/apps/sim/tools/ketch/types.ts @@ -0,0 +1,117 @@ +import type { OutputProperty, ToolResponse } from '@/tools/types' + +/** + * Shared output property definitions for Ketch API responses. + * Based on Ketch Web API: https://github.com/ketch-sdk/ketch-web-api + * Types reference: https://github.com/ketch-sdk/ketch-types + */ + +export const CONSENT_PURPOSE_OUTPUT_PROPERTIES = { + allowed: { + type: 'string', + description: 'Consent status for the purpose: "granted" or "denied"', + }, + legalBasisCode: { + type: 'string', + description: + 'Legal basis code (e.g., "consent_optin", "consent_optout", "disclosure", "other")', + optional: true, + }, +} as const satisfies Record + +export interface KetchGetConsentParams { + organizationCode: string + propertyCode: string + environmentCode: string + jurisdictionCode?: string + identities: Record + purposes?: Record> +} + +export interface KetchGetConsentResponse extends ToolResponse { + output: { + purposes: Record + vendors: Record | null + } +} + +export interface KetchSetConsentParams { + organizationCode: string + propertyCode: string + environmentCode: string + jurisdictionCode?: string + identities: Record + purposes: Record + collectedAt?: number +} + +export interface KetchSetConsentResponse extends ToolResponse { + output: { + purposes: Record + } +} + +export interface KetchInvokeRightParams { + organizationCode: string + propertyCode: string + environmentCode: string + jurisdictionCode: string + rightCode: string + identities: Record + userData?: { + email?: string + firstName?: string + lastName?: string + } +} + +export interface KetchInvokeRightResponse extends ToolResponse { + output: { + success: boolean + message: string | null + } +} + +export interface SubscriptionControlSetting { + status: string +} + +export interface SubscriptionTopicContactMethodSetting { + status: string +} + +export interface KetchGetSubscriptionsParams { + organizationCode: string + propertyCode: string + environmentCode: string + identities: Record +} + +export interface KetchGetSubscriptionsResponse extends ToolResponse { + output: { + topics: Record> + controls: Record + } +} + +export interface KetchSetSubscriptionsParams { + organizationCode: string + propertyCode: string + environmentCode: string + identities: Record + topics?: Record> + controls?: Record +} + +export interface KetchSetSubscriptionsResponse extends ToolResponse { + output: { + success: boolean + } +} + +export type KetchResponse = + | KetchGetConsentResponse + | KetchSetConsentResponse + | KetchInvokeRightResponse + | KetchGetSubscriptionsResponse + | KetchSetSubscriptionsResponse diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 99efb6dcbbc..f340600ed0c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1259,6 +1259,13 @@ import { kalshiGetTradesTool, kalshiGetTradesV2Tool, } from '@/tools/kalshi' +import { + ketchGetConsentTool, + ketchGetSubscriptionsTool, + ketchInvokeRightTool, + ketchSetConsentTool, + ketchSetSubscriptionsTool, +} from '@/tools/ketch' import { knowledgeCreateDocumentTool, knowledgeDeleteChunkTool, @@ -2678,6 +2685,11 @@ export const tools: Record = { hex_run_project: hexRunProjectTool, hex_update_project: hexUpdateProjectTool, jina_read_url: jinaReadUrlTool, + ketch_get_consent: ketchGetConsentTool, + ketch_get_subscriptions: ketchGetSubscriptionsTool, + ketch_invoke_right: ketchInvokeRightTool, + ketch_set_consent: ketchSetConsentTool, + ketch_set_subscriptions: ketchSetSubscriptionsTool, jina_search: jinaSearchTool, linkup_search: linkupSearchTool, loops_create_contact: loopsCreateContactTool, From 3790a4b01d229c44dc65b3dded7da2860ae9fa6f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Mar 2026 15:29:40 -0700 Subject: [PATCH 2/4] fix(ketch): add response.ok guards and fix registry ordering --- apps/sim/tools/ketch/get_consent.ts | 17 +++++++++++++++++ apps/sim/tools/ketch/get_subscriptions.ts | 17 +++++++++++++++++ apps/sim/tools/ketch/set_consent.ts | 16 ++++++++++++++++ apps/sim/tools/ketch/set_subscriptions.ts | 19 ++++++++++--------- apps/sim/tools/registry.ts | 2 +- 5 files changed, 61 insertions(+), 10 deletions(-) diff --git a/apps/sim/tools/ketch/get_consent.ts b/apps/sim/tools/ketch/get_consent.ts index 75cfc0d4d12..93be7585139 100644 --- a/apps/sim/tools/ketch/get_consent.ts +++ b/apps/sim/tools/ketch/get_consent.ts @@ -70,6 +70,23 @@ export const getConsentTool: ToolConfig { + if (!response.ok) { + let errorMessage = `Request failed with status ${response.status}` + try { + const data = await response.json() + errorMessage = data.message ?? data.error ?? errorMessage + } catch { + // No JSON body in error response + } + return { + success: false, + output: { + purposes: {}, + vendors: null, + }, + } + } + const data = await response.json() return { success: true, diff --git a/apps/sim/tools/ketch/get_subscriptions.ts b/apps/sim/tools/ketch/get_subscriptions.ts index a02cc598a45..969aea704d3 100644 --- a/apps/sim/tools/ketch/get_subscriptions.ts +++ b/apps/sim/tools/ketch/get_subscriptions.ts @@ -58,6 +58,23 @@ export const getSubscriptionsTool: ToolConfig< }, transformResponse: async (response: Response) => { + if (!response.ok) { + let errorMessage = `Request failed with status ${response.status}` + try { + const data = await response.json() + errorMessage = data.message ?? data.error ?? errorMessage + } catch { + // No JSON body in error response + } + return { + success: false, + output: { + topics: {}, + controls: {}, + }, + } + } + const data = await response.json() return { success: true, diff --git a/apps/sim/tools/ketch/set_consent.ts b/apps/sim/tools/ketch/set_consent.ts index 55a0c973427..93b03fcb28f 100644 --- a/apps/sim/tools/ketch/set_consent.ts +++ b/apps/sim/tools/ketch/set_consent.ts @@ -78,6 +78,22 @@ export const setConsentTool: ToolConfig { + if (!response.ok) { + let errorMessage = `Request failed with status ${response.status}` + try { + const data = await response.json() + errorMessage = data.message ?? data.error ?? errorMessage + } catch { + // No JSON body in error response + } + return { + success: false, + output: { + purposes: {}, + }, + } + } + if (response.status === 204) { return { success: true, diff --git a/apps/sim/tools/ketch/set_subscriptions.ts b/apps/sim/tools/ketch/set_subscriptions.ts index a71d18015fd..3823648da3e 100644 --- a/apps/sim/tools/ketch/set_subscriptions.ts +++ b/apps/sim/tools/ketch/set_subscriptions.ts @@ -77,21 +77,22 @@ export const setSubscriptionsTool: ToolConfig< }, transformResponse: async (response: Response) => { - if (response.status === 204) { + if (!response.ok) { + let errorMessage = `Request failed with status ${response.status}` + try { + const data = await response.json() + errorMessage = data.message ?? data.error ?? errorMessage + } catch { + // No JSON body in error response + } return { - success: true, + success: false, output: { - success: true, + success: false, }, } } - try { - await response.json() - } catch { - // 204-like response with no body - } - return { success: true, output: { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index f340600ed0c..7dca2bdebd3 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2685,12 +2685,12 @@ export const tools: Record = { hex_run_project: hexRunProjectTool, hex_update_project: hexUpdateProjectTool, jina_read_url: jinaReadUrlTool, + jina_search: jinaSearchTool, ketch_get_consent: ketchGetConsentTool, ketch_get_subscriptions: ketchGetSubscriptionsTool, ketch_invoke_right: ketchInvokeRightTool, ketch_set_consent: ketchSetConsentTool, ketch_set_subscriptions: ketchSetSubscriptionsTool, - jina_search: jinaSearchTool, linkup_search: linkupSearchTool, loops_create_contact: loopsCreateContactTool, loops_create_contact_property: loopsCreateContactPropertyTool, From 166f53b7478210da6baaf7b064092414e6efb5a8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 26 Mar 2026 15:36:08 -0700 Subject: [PATCH 3/4] fix(ketch): include errorMessage in error response output for all tools --- apps/sim/tools/ketch/get_consent.ts | 1 + apps/sim/tools/ketch/get_subscriptions.ts | 1 + apps/sim/tools/ketch/set_consent.ts | 1 + apps/sim/tools/ketch/set_subscriptions.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/apps/sim/tools/ketch/get_consent.ts b/apps/sim/tools/ketch/get_consent.ts index 93be7585139..454439d640d 100644 --- a/apps/sim/tools/ketch/get_consent.ts +++ b/apps/sim/tools/ketch/get_consent.ts @@ -81,6 +81,7 @@ export const getConsentTool: ToolConfig Date: Thu, 26 Mar 2026 15:45:45 -0700 Subject: [PATCH 4/4] fix(ketch): wire optional purposes filter for get_consent operation --- apps/sim/blocks/blocks/ketch.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/sim/blocks/blocks/ketch.ts b/apps/sim/blocks/blocks/ketch.ts index 8b359ed0086..8f9af1842ae 100644 --- a/apps/sim/blocks/blocks/ketch.ts +++ b/apps/sim/blocks/blocks/ketch.ts @@ -74,6 +74,15 @@ export const KetchBlock: BlockConfig = { language: 'json', required: true, }, + { + id: 'purposesFilter', + title: 'Purposes Filter', + type: 'code', + placeholder: '{"analytics": {}, "marketing": {}}', + language: 'json', + condition: { field: 'operation', value: 'get_consent' }, + mode: 'advanced', + }, { id: 'purposes', title: 'Purposes', @@ -164,6 +173,13 @@ export const KetchBlock: BlockConfig = { : params.identities } + if (params.operation === 'get_consent' && params.purposesFilter) { + result.purposes = + typeof params.purposesFilter === 'string' + ? JSON.parse(params.purposesFilter) + : params.purposesFilter + } + if (params.operation === 'set_consent' && params.purposes) { result.purposes = typeof params.purposes === 'string' ? JSON.parse(params.purposes) : params.purposes