Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before POST /v1/campaigns when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign.
This endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy.
Create a draft campaign from the public steps format.
How to structure the request:
Start steps with the first action that should happen.
Keep adding the next action in the order it should run.
Use condition when you want to branch on lead data.
Use linkedin_connection with waitForAcceptance when you want to branch on connection acceptance after sending a connection request. Omit waitForAcceptance for a fire-and-forget connection request.
Use linkedin_message with content, attachment, or both. Attachment-only messages are valid.
Use whatsapp_message only when the workspace and sending identity are configured for WhatsApp.
Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches.
Senders:
Pass identityIds (primary first) to send from several identities; the campaign spreads its contacts across them. identityId is the single-sender shorthand.
More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with details.codeMULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED.
Use GET /v1/identities to find identity IDs. allowedEmails only applies to single-sender campaigns.
AI-generated messages (message library):
email, linkedin_message, linkedin_inmail, and linkedin_connection steps accept aiMessageId instead of manual content — the linked AI message writes a personalized message per contact at send time.
To reuse a prompt-library (“message library”) prompt, list entries with GET /v1/prompt-library/ai-messages and pass the entry id as the step aiMessageId. The step gets its own copy automatically.
To reuse or create a workspace AI message, use GET /v1/ai-variables/ai-messages or POST /v1/ai-variables/ai-messages (full control over prompt, toneId, model, outputLength, and splitMessages), then pass its id as aiMessageId.
The AI message channel must match the step type (EMAIL, LINKEDIN, LINKEDIN_INMAIL, or LINKEDIN_CONNECTION).
Never combine aiMessageId with manual content or subject on the same step — for email and linkedin_inmail the AI message generates the subject too.
Email threading (Reply / New thread):
Set thread on an email step to pick how it is sent. reply sends it in the thread of the previous email, like the editor’s Reply option, and needs no subject. new_thread starts a separate thread under the step’s own subject.
When thread is omitted, a follow-up email without subject replies and one with subject is sent under that subject. Set thread explicitly whenever the choice matters, especially for aiMessageId follow-ups, which otherwise go out under their generated subject.
Before you send a request:
Send an ordered steps array that describes the campaign flow from first action to last action.
Use branch arrays on condition and linkedin_connection steps to describe branch-specific work.
Place shared follow-up steps after a branching step when both live branches should continue together.
waitForAcceptance.unit must be days and waitForAcceptance.value must be an integer greater than or equal to 1.
Use GET /v1/tasks/owners first when your campaign includes task steps. If owners are returned, every task step must include ownerId.
Validation errors use full paths like steps[0].onTrue[1].subject so nested input issues are easy to fix.
Personalization placeholders use Enginy single-brace syntax such as {firstName}, {companyName}, and {identity.name}. Do not send Handlebars-style placeholders like {{firstName}}.
Placeholders must reference real fields — discover them with GET /v1/contacts/fields. Unknown placeholders are rejected with a 422. The company aliases {company}, {companyName}, and {companyDomain} are accepted and stored as the canonical company columns.
To modify an existing DRAFT campaign in place, prefer PATCH /v1/campaign/{campaignId} over creating a new one. To rewrite into a new campaign, start from the steps returned by GET /v1/campaign/{campaignId}. Either way the read-only stepId, connectionId, nextConnectionId, and advanced fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their aiMessageId binding.
Supported condition.type values:
has_linkedin_profile: onTrue runs when the lead has a LinkedIn profile.
has_professional_email: onTrue runs when the lead has a professional email.
is_already_connected: onTrue runs when the lead is already a LinkedIn connection.
has_been_contacted: onTrue runs when the lead has already been contacted.
has_whatsapp_account: onTrue runs when the lead has a WhatsApp account.
email_opened: onTrue runs when the lead opens the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit).
email_clicked: onTrue runs when the lead clicks a link in the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit).
linkedin_message_viewed: onTrue runs when the lead views the preceding LinkedIn message within waitFor. Must follow a LinkedIn message step (linkedin_message, linkedin_message_bundle, linkedin_voice_message, or linkedin_inmail). Requires waitFor (value, unit).
task_completed: onTrue runs when the preceding task step is marked complete within waitFor. Must follow a task step. Requires waitFor (value, unit).
connection_accepted: onTrue runs when the lead accepts a previous LinkedIn connection request within waitFor. Requires waitFor (value, unit).
lead_field: onTrue runs when the contact field matches the operator (and value, where applicable). Use this to branch on any built-in column or custom field. Requires field and operator. value is required for every operator except IS_EMPTY and IS_NOT_EMPTY. Supported operators: EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS, IS_EMPTY, IS_NOT_EMPTY.
Branch fields:
condition uses onTrue and onFalse.
linkedin_connection uses onAccepted and onNotAccepted only when waitForAcceptance is set.
Use [{ "type": "end" }] when a branch should stop explicitly.
Routing into another campaign:
Use add_to_another_campaign with a campaignId to route the lead into an existing campaign in the same workspace.
The referenced campaignId must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each campaignId to the equivalent campaign in the destination workspace before posting.
Can be followed by additional steps in the same branch.
For examples and a full step-by-step guide, see /api-reference/campaigns-guide in the docs site.
Ordered campaign steps. Use branch arrays like onTrue, onFalse, onAccepted, and onNotAccepted for branching. GET includes saved manual template content, with HTML email bodies. Steps PATCH rejects dynamic template-email bodies with 422 before replacement. When resubmitting steps, the read-only stepId, connectionId, nextConnectionId, and advanced fields are accepted and ignored.
Sender identity IDs, primary first (repeats are ignored). The campaign spreads its contacts across these senders. More than one sender requires the multi-identity campaigns beta on the workspace. When identityId is also sent it must equal the first entry.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Enginy API Docs","description":"Developer docs for Enginy, the B2B prospecting platform: REST API, hosted MCP server, Advanced Workflows reference, and custom CRM integration.","colors":{"primary":"#1EC371","light":"#47E194","dark":"#171717"},"logo":{"light":"https://mintcdn.com/genesyai/WDIQAoU7a8DJAxu4/logo/enginy-dark.svg?fit=max\u0026auto=format\u0026n=WDIQAoU7a8DJAxu4\u0026q=85\u0026s=939acf98e42633af0faae66d57713e73","dark":"https://mintcdn.com/genesyai/WDIQAoU7a8DJAxu4/logo/enginy-light.svg?fit=max\u0026auto=format\u0026n=WDIQAoU7a8DJAxu4\u0026q=85\u0026s=e25861810973f70a5bcf4ff3e9e07b2a"},"favicon":"https://mintcdn.com/genesyai/WDIQAoU7a8DJAxu4/favicon.svg?fit=max\u0026auto=format\u0026n=WDIQAoU7a8DJAxu4\u0026q=85\u0026s=29e39121b8aed81a43a6f121813c4ee0","navbar":{"links":[{"href":"mailto:support@enginy.ai","label":"Support"}],"primary":{"type":"button","label":"App","href":"https://app.enginy.ai"}},"footer":{"socials":{"linkedin":"https://www.linkedin.com/company/enginy-ai"}},"contextual":{"options":["copy","view","chatgpt","claude","perplexity","mcp","cursor","vscode"],"display":"header"},"styling":{"latex":false},"navigation":{"global":{"anchors":[{"anchor":"Help Center","icon":"book-open-cover","href":"https://help.enginy.ai/"},{"anchor":"API Dashboard","icon":"key","href":"https://app.enginy.ai/api-access"}]},"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"genesyai","hasAudioNarration":false,"actualSubdomain":"genesyai","customDomains":["docs.enginy.ai"],"gitSource":{"type":"github","owner":"genesy-ai","repo":"leads-gpt","deployBranch":"main","contentDirectory":"openapi-docs","isPrivate":true,"mountPath":"","label":""},"chroma":{"liveDeploymentHistoryId":"6adb0409aa8f362a306d917d","collectionId":"1b918f07-3295-4564-8126-46ee4b0bb6ae","schemaVersion":"v3","latestBuiltDeploymentHistoryId":"6adb0409aa8f362a306d917d"},"entitlements":{},"buildId":"6adb096d35d195450e7200af","clientVersion":"0.0.3966","searchFilterCounts":[{"language":"default","tag":"API reference","count":226},{"language":"default","tag":"Endpoints","count":213},{"language":"default","tag":"AI Variables","count":46},{"language":"default","tag":"Workflows","count":39},{"language":"default","tag":"Custom CRM","count":24},{"language":"default","tag":"Contacts","count":21},{"language":"default","tag":"Lists","count":21},{"language":"default","tag":"Companies","count":20},{"language":"default","tag":"Campaigns","count":19},{"language":"default","tag":"Tasks","count":17}]},"assistantConfig":{},"hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-v7-file-tree","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"pause-universal-search-reconcile","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"per-site-search-delta","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-private-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-nav-generations","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"byok-sep-25","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"vite-origin","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-agent-model-picker","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-updated-9-15","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"isAtlas":false,"googleFontsInlined":false},"customCss":[],"page":{"pagePath":"api-reference/campaigns/validate-campaign","slug":"/api-reference/campaigns/validate-campaign","pageMetadata":{"title":"Validate campaign","description":"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before `POST /v1/campaigns` when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign.\n\nThis endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy.\n\nCreate a draft campaign from the public `steps` format.\n\nHow to structure the request:\n- Start `steps` with the first action that should happen.\n- Keep adding the next action in the order it should run.\n- Use `condition` when you want to branch on lead data.\n- Use `linkedin_connection` with `waitForAcceptance` when you want to branch on connection acceptance after sending a connection request. Omit `waitForAcceptance` for a fire-and-forget connection request.\n- Use `linkedin_message` with `content`, `attachment`, or both. Attachment-only messages are valid.\n- Use `whatsapp_message` only when the workspace and sending identity are configured for WhatsApp.\n- Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches.\n\nSenders:\n- Pass `identityIds` (primary first) to send from several identities; the campaign spreads its contacts across them. `identityId` is the single-sender shorthand.\n- More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with `details.code` `MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED`.\n- Use `GET /v1/identities` to find identity IDs. `allowedEmails` only applies to single-sender campaigns.\n\nAI-generated messages (message library):\n- `email`, `linkedin_message`, `linkedin_inmail`, and `linkedin_connection` steps accept `aiMessageId` instead of manual `content` — the linked AI message writes a personalized message per contact at send time.\n- To reuse a prompt-library (\"message library\") prompt, list entries with `GET /v1/prompt-library/ai-messages` and pass the entry `id` as the step `aiMessageId`. The step gets its own copy automatically.\n- To reuse or create a workspace AI message, use `GET /v1/ai-variables/ai-messages` or `POST /v1/ai-variables/ai-messages` (full control over `prompt`, `toneId`, `model`, `outputLength`, and `splitMessages`), then pass its `id` as `aiMessageId`.\n- The AI message `channel` must match the step type (`EMAIL`, `LINKEDIN`, `LINKEDIN_INMAIL`, or `LINKEDIN_CONNECTION`).\n- Never combine `aiMessageId` with manual `content` or `subject` on the same step — for `email` and `linkedin_inmail` the AI message generates the subject too.\n\nEmail threading (Reply / New thread):\n- Set `thread` on an `email` step to pick how it is sent. `reply` sends it in the thread of the previous email, like the editor's Reply option, and needs no `subject`. `new_thread` starts a separate thread under the step's own `subject`.\n- When `thread` is omitted, a follow-up email without `subject` replies and one with `subject` is sent under that subject. Set `thread` explicitly whenever the choice matters, especially for `aiMessageId` follow-ups, which otherwise go out under their generated subject.\n\nBefore you send a request:\n- Send an ordered `steps` array that describes the campaign flow from first action to last action.\n- Use branch arrays on `condition` and `linkedin_connection` steps to describe branch-specific work.\n- Place shared follow-up steps after a branching step when both live branches should continue together.\n- `waitForAcceptance.unit` must be `days` and `waitForAcceptance.value` must be an integer greater than or equal to `1`.\n- Use `GET /v1/tasks/owners` first when your campaign includes `task` steps. If owners are returned, every `task` step must include `ownerId`.\n- Validation errors use full paths like `steps[0].onTrue[1].subject` so nested input issues are easy to fix.\n- Personalization placeholders use Enginy single-brace syntax such as `{firstName}`, `{companyName}`, and `{identity.name}`. Do not send Handlebars-style placeholders like `{{firstName}}`.\n- Placeholders must reference real fields — discover them with `GET /v1/contacts/fields`. Unknown placeholders are rejected with a 422. The company aliases `{company}`, `{companyName}`, and `{companyDomain}` are accepted and stored as the canonical company columns.\n- To modify an existing DRAFT campaign in place, prefer `PATCH /v1/campaign/{campaignId}` over creating a new one. To rewrite into a new campaign, start from the `steps` returned by `GET /v1/campaign/{campaignId}`. Either way the read-only `stepId`, `connectionId`, `nextConnectionId`, and `advanced` fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their `aiMessageId` binding.\n\nSupported `condition.type` values:\n- `has_linkedin_profile`: `onTrue` runs when the lead has a LinkedIn profile.\n- `has_professional_email`: `onTrue` runs when the lead has a professional email.\n- `is_already_connected`: `onTrue` runs when the lead is already a LinkedIn connection.\n- `has_been_contacted`: `onTrue` runs when the lead has already been contacted.\n- `has_whatsapp_account`: `onTrue` runs when the lead has a WhatsApp account.\n- `email_opened`: `onTrue` runs when the lead opens the preceding email within `waitFor`. Must follow an `email` step. Requires `waitFor` (`value`, `unit`).\n- `email_clicked`: `onTrue` runs when the lead clicks a link in the preceding email within `waitFor`. Must follow an `email` step. Requires `waitFor` (`value`, `unit`).\n- `linkedin_message_viewed`: `onTrue` runs when the lead views the preceding LinkedIn message within `waitFor`. Must follow a LinkedIn message step (`linkedin_message`, `linkedin_message_bundle`, `linkedin_voice_message`, or `linkedin_inmail`). Requires `waitFor` (`value`, `unit`).\n- `task_completed`: `onTrue` runs when the preceding `task` step is marked complete within `waitFor`. Must follow a `task` step. Requires `waitFor` (`value`, `unit`).\n- `connection_accepted`: `onTrue` runs when the lead accepts a previous LinkedIn connection request within `waitFor`. Requires `waitFor` (`value`, `unit`).\n- `lead_field`: `onTrue` runs when the contact `field` matches the `operator` (and `value`, where applicable). Use this to branch on any built-in column or custom field. Requires `field` and `operator`. `value` is required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`. Supported operators: `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `NOT_CONTAINS`, `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_OR_EQUALS`, `LESS_THAN_OR_EQUALS`, `IS_EMPTY`, `IS_NOT_EMPTY`.\n\nBranch fields:\n- `condition` uses `onTrue` and `onFalse`.\n- `linkedin_connection` uses `onAccepted` and `onNotAccepted` only when `waitForAcceptance` is set.\n- Use `[{ \"type\": \"end\" }]` when a branch should stop explicitly.\n\nRouting into another campaign:\n- Use `add_to_another_campaign` with a `campaignId` to route the lead into an existing campaign in the same workspace.\n- The referenced `campaignId` must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each `campaignId` to the equivalent campaign in the destination workspace before posting.\n- Can be followed by additional steps in the same branch.\n\nFor examples and a full step-by-step guide, see `/api-reference/campaigns-guide` in the docs site.\n\n > **Required scope:** `CAMPAIGNS_WRITE`\n >\n > **Rate limit:** 30 requests per minute","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/campaigns/validate","href":"/api-reference/campaigns/validate-campaign","autogeneratedByOpenApi":true},"description":{"compiledSource":"\"use strict\";\nconst {Fragment: _Fragment, jsx: _jsx, jsxs: _jsxs} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n blockquote: \"blockquote\",\n code: \"code\",\n li: \"li\",\n p: \"p\",\n strong: \"strong\",\n ul: \"ul\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsxs(_Fragment, {\n children: [_jsxs(_components.p, {\n children: [\"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before \", _jsx(_components.code, {\n children: \"POST /v1/campaigns\"\n }), \" when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign.\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"This endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy.\"\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"Create a draft campaign from the public \", _jsx(_components.code, {\n children: \"steps\"\n }), \" format.\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"How to structure the request:\"\n }), \"\\n\", _jsxs(_components.ul, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [\"Start \", _jsx(_components.code, {\n children: \"steps\"\n }), \" with the first action that should happen.\"]\n }), \"\\n\", _jsx(_components.li, {\n children: \"Keep adding the next action in the order it should run.\"\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Use \", _jsx(_components.code, {\n children: \"condition\"\n }), \" when you want to branch on lead data.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Use \", _jsx(_components.code, {\n children: \"linkedin_connection\"\n }), \" with \", _jsx(_components.code, {\n children: \"waitForAcceptance\"\n }), \" when you want to branch on connection acceptance after sending a connection request. Omit \", _jsx(_components.code, {\n children: \"waitForAcceptance\"\n }), \" for a fire-and-forget connection request.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Use \", _jsx(_components.code, {\n children: \"linkedin_message\"\n }), \" with \", _jsx(_components.code, {\n children: \"content\"\n }), \", \", _jsx(_components.code, {\n children: \"attachment\"\n }), \", or both. Attachment-only messages are valid.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Use \", _jsx(_components.code, {\n children: \"whatsapp_message\"\n }), \" only when the workspace and sending identity are configured for WhatsApp.\"]\n }), \"\\n\", _jsx(_components.li, {\n children: \"Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches.\"\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"Senders:\"\n }), \"\\n\", _jsxs(_components.ul, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [\"Pass \", _jsx(_components.code, {\n children: \"identityIds\"\n }), \" (primary first) to send from several identities; the campaign spreads its contacts across them. \", _jsx(_components.code, {\n children: \"identityId\"\n }), \" is the single-sender shorthand.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with \", _jsx(_components.code, {\n children: \"details.code\"\n }), \" \", _jsx(_components.code, {\n children: \"MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED\"\n }), \".\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Use \", _jsx(_components.code, {\n children: \"GET /v1/identities\"\n }), \" to find identity IDs. \", _jsx(_components.code, {\n children: \"allowedEmails\"\n }), \" only applies to single-sender campaigns.\"]\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"AI-generated messages (message library):\"\n }), \"\\n\", _jsxs(_components.ul, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"email\"\n }), \", \", _jsx(_components.code, {\n children: \"linkedin_message\"\n }), \", \", _jsx(_components.code, {\n children: \"linkedin_inmail\"\n }), \", and \", _jsx(_components.code, {\n children: \"linkedin_connection\"\n }), \" steps accept \", _jsx(_components.code, {\n children: \"aiMessageId\"\n }), \" instead of manual \", _jsx(_components.code, {\n children: \"content\"\n }), \" — the linked AI message writes a personalized message per contact at send time.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"To reuse a prompt-library (“message library”) prompt, list entries with \", _jsx(_components.code, {\n children: \"GET /v1/prompt-library/ai-messages\"\n }), \" and pass the entry \", _jsx(_components.code, {\n children: \"id\"\n }), \" as the step \", _jsx(_components.code, {\n children: \"aiMessageId\"\n }), \". The step gets its own copy automatically.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"To reuse or create a workspace AI message, use \", _jsx(_components.code, {\n children: \"GET /v1/ai-variables/ai-messages\"\n }), \" or \", _jsx(_components.code, {\n children: \"POST /v1/ai-variables/ai-messages\"\n }), \" (full control over \", _jsx(_components.code, {\n children: \"prompt\"\n }), \", \", _jsx(_components.code, {\n children: \"toneId\"\n }), \", \", _jsx(_components.code, {\n children: \"model\"\n }), \", \", _jsx(_components.code, {\n children: \"outputLength\"\n }), \", and \", _jsx(_components.code, {\n children: \"splitMessages\"\n }), \"), then pass its \", _jsx(_components.code, {\n children: \"id\"\n }), \" as \", _jsx(_components.code, {\n children: \"aiMessageId\"\n }), \".\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"The AI message \", _jsx(_components.code, {\n children: \"channel\"\n }), \" must match the step type (\", _jsx(_components.code, {\n children: \"EMAIL\"\n }), \", \", _jsx(_components.code, {\n children: \"LINKEDIN\"\n }), \", \", _jsx(_components.code, {\n children: \"LINKEDIN_INMAIL\"\n }), \", or \", _jsx(_components.code, {\n children: \"LINKEDIN_CONNECTION\"\n }), \").\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Never combine \", _jsx(_components.code, {\n children: \"aiMessageId\"\n }), \" with manual \", _jsx(_components.code, {\n children: \"content\"\n }), \" or \", _jsx(_components.code, {\n children: \"subject\"\n }), \" on the same step — for \", _jsx(_components.code, {\n children: \"email\"\n }), \" and \", _jsx(_components.code, {\n children: \"linkedin_inmail\"\n }), \" the AI message generates the subject too.\"]\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"Email threading (Reply / New thread):\"\n }), \"\\n\", _jsxs(_components.ul, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [\"Set \", _jsx(_components.code, {\n children: \"thread\"\n }), \" on an \", _jsx(_components.code, {\n children: \"email\"\n }), \" step to pick how it is sent. \", _jsx(_components.code, {\n children: \"reply\"\n }), \" sends it in the thread of the previous email, like the editor’s Reply option, and needs no \", _jsx(_components.code, {\n children: \"subject\"\n }), \". \", _jsx(_components.code, {\n children: \"new_thread\"\n }), \" starts a separate thread under the step’s own \", _jsx(_components.code, {\n children: \"subject\"\n }), \".\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"When \", _jsx(_components.code, {\n children: \"thread\"\n }), \" is omitted, a follow-up email without \", _jsx(_components.code, {\n children: \"subject\"\n }), \" replies and one with \", _jsx(_components.code, {\n children: \"subject\"\n }), \" is sent under that subject. Set \", _jsx(_components.code, {\n children: \"thread\"\n }), \" explicitly whenever the choice matters, especially for \", _jsx(_components.code, {\n children: \"aiMessageId\"\n }), \" follow-ups, which otherwise go out under their generated subject.\"]\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"Before you send a request:\"\n }), \"\\n\", _jsxs(_components.ul, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [\"Send an ordered \", _jsx(_components.code, {\n children: \"steps\"\n }), \" array that describes the campaign flow from first action to last action.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Use branch arrays on \", _jsx(_components.code, {\n children: \"condition\"\n }), \" and \", _jsx(_components.code, {\n children: \"linkedin_connection\"\n }), \" steps to describe branch-specific work.\"]\n }), \"\\n\", _jsx(_components.li, {\n children: \"Place shared follow-up steps after a branching step when both live branches should continue together.\"\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"waitForAcceptance.unit\"\n }), \" must be \", _jsx(_components.code, {\n children: \"days\"\n }), \" and \", _jsx(_components.code, {\n children: \"waitForAcceptance.value\"\n }), \" must be an integer greater than or equal to \", _jsx(_components.code, {\n children: \"1\"\n }), \".\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Use \", _jsx(_components.code, {\n children: \"GET /v1/tasks/owners\"\n }), \" first when your campaign includes \", _jsx(_components.code, {\n children: \"task\"\n }), \" steps. If owners are returned, every \", _jsx(_components.code, {\n children: \"task\"\n }), \" step must include \", _jsx(_components.code, {\n children: \"ownerId\"\n }), \".\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Validation errors use full paths like \", _jsx(_components.code, {\n children: \"steps[0].onTrue[1].subject\"\n }), \" so nested input issues are easy to fix.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Personalization placeholders use Enginy single-brace syntax such as \", _jsx(_components.code, {\n children: \"{firstName}\"\n }), \", \", _jsx(_components.code, {\n children: \"{companyName}\"\n }), \", and \", _jsx(_components.code, {\n children: \"{identity.name}\"\n }), \". Do not send Handlebars-style placeholders like \", _jsx(_components.code, {\n children: \"{{firstName}}\"\n }), \".\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Placeholders must reference real fields — discover them with \", _jsx(_components.code, {\n children: \"GET /v1/contacts/fields\"\n }), \". Unknown placeholders are rejected with a 422. The company aliases \", _jsx(_components.code, {\n children: \"{company}\"\n }), \", \", _jsx(_components.code, {\n children: \"{companyName}\"\n }), \", and \", _jsx(_components.code, {\n children: \"{companyDomain}\"\n }), \" are accepted and stored as the canonical company columns.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"To modify an existing DRAFT campaign in place, prefer \", _jsx(_components.code, {\n children: \"PATCH /v1/campaign/{campaignId}\"\n }), \" over creating a new one. To rewrite into a new campaign, start from the \", _jsx(_components.code, {\n children: \"steps\"\n }), \" returned by \", _jsx(_components.code, {\n children: \"GET /v1/campaign/{campaignId}\"\n }), \". Either way the read-only \", _jsx(_components.code, {\n children: \"stepId\"\n }), \", \", _jsx(_components.code, {\n children: \"connectionId\"\n }), \", \", _jsx(_components.code, {\n children: \"nextConnectionId\"\n }), \", and \", _jsx(_components.code, {\n children: \"advanced\"\n }), \" fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their \", _jsx(_components.code, {\n children: \"aiMessageId\"\n }), \" binding.\"]\n }), \"\\n\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"Supported \", _jsx(_components.code, {\n children: \"condition.type\"\n }), \" values:\"]\n }), \"\\n\", _jsxs(_components.ul, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"has_linkedin_profile\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead has a LinkedIn profile.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"has_professional_email\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead has a professional email.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"is_already_connected\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead is already a LinkedIn connection.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"has_been_contacted\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead has already been contacted.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"has_whatsapp_account\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead has a WhatsApp account.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"email_opened\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead opens the preceding email within \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \". Must follow an \", _jsx(_components.code, {\n children: \"email\"\n }), \" step. Requires \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \" (\", _jsx(_components.code, {\n children: \"value\"\n }), \", \", _jsx(_components.code, {\n children: \"unit\"\n }), \").\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"email_clicked\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead clicks a link in the preceding email within \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \". Must follow an \", _jsx(_components.code, {\n children: \"email\"\n }), \" step. Requires \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \" (\", _jsx(_components.code, {\n children: \"value\"\n }), \", \", _jsx(_components.code, {\n children: \"unit\"\n }), \").\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"linkedin_message_viewed\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead views the preceding LinkedIn message within \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \". Must follow a LinkedIn message step (\", _jsx(_components.code, {\n children: \"linkedin_message\"\n }), \", \", _jsx(_components.code, {\n children: \"linkedin_message_bundle\"\n }), \", \", _jsx(_components.code, {\n children: \"linkedin_voice_message\"\n }), \", or \", _jsx(_components.code, {\n children: \"linkedin_inmail\"\n }), \"). Requires \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \" (\", _jsx(_components.code, {\n children: \"value\"\n }), \", \", _jsx(_components.code, {\n children: \"unit\"\n }), \").\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"task_completed\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the preceding \", _jsx(_components.code, {\n children: \"task\"\n }), \" step is marked complete within \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \". Must follow a \", _jsx(_components.code, {\n children: \"task\"\n }), \" step. Requires \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \" (\", _jsx(_components.code, {\n children: \"value\"\n }), \", \", _jsx(_components.code, {\n children: \"unit\"\n }), \").\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"connection_accepted\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the lead accepts a previous LinkedIn connection request within \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \". Requires \", _jsx(_components.code, {\n children: \"waitFor\"\n }), \" (\", _jsx(_components.code, {\n children: \"value\"\n }), \", \", _jsx(_components.code, {\n children: \"unit\"\n }), \").\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"lead_field\"\n }), \": \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" runs when the contact \", _jsx(_components.code, {\n children: \"field\"\n }), \" matches the \", _jsx(_components.code, {\n children: \"operator\"\n }), \" (and \", _jsx(_components.code, {\n children: \"value\"\n }), \", where applicable). Use this to branch on any built-in column or custom field. Requires \", _jsx(_components.code, {\n children: \"field\"\n }), \" and \", _jsx(_components.code, {\n children: \"operator\"\n }), \". \", _jsx(_components.code, {\n children: \"value\"\n }), \" is required for every operator except \", _jsx(_components.code, {\n children: \"IS_EMPTY\"\n }), \" and \", _jsx(_components.code, {\n children: \"IS_NOT_EMPTY\"\n }), \". Supported operators: \", _jsx(_components.code, {\n children: \"EQUALS\"\n }), \", \", _jsx(_components.code, {\n children: \"NOT_EQUALS\"\n }), \", \", _jsx(_components.code, {\n children: \"CONTAINS\"\n }), \", \", _jsx(_components.code, {\n children: \"NOT_CONTAINS\"\n }), \", \", _jsx(_components.code, {\n children: \"GREATER_THAN\"\n }), \", \", _jsx(_components.code, {\n children: \"LESS_THAN\"\n }), \", \", _jsx(_components.code, {\n children: \"GREATER_THAN_OR_EQUALS\"\n }), \", \", _jsx(_components.code, {\n children: \"LESS_THAN_OR_EQUALS\"\n }), \", \", _jsx(_components.code, {\n children: \"IS_EMPTY\"\n }), \", \", _jsx(_components.code, {\n children: \"IS_NOT_EMPTY\"\n }), \".\"]\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"Branch fields:\"\n }), \"\\n\", _jsxs(_components.ul, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"condition\"\n }), \" uses \", _jsx(_components.code, {\n children: \"onTrue\"\n }), \" and \", _jsx(_components.code, {\n children: \"onFalse\"\n }), \".\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [_jsx(_components.code, {\n children: \"linkedin_connection\"\n }), \" uses \", _jsx(_components.code, {\n children: \"onAccepted\"\n }), \" and \", _jsx(_components.code, {\n children: \"onNotAccepted\"\n }), \" only when \", _jsx(_components.code, {\n children: \"waitForAcceptance\"\n }), \" is set.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"Use \", _jsx(_components.code, {\n children: \"[{ \\\"type\\\": \\\"end\\\" }]\"\n }), \" when a branch should stop explicitly.\"]\n }), \"\\n\"]\n }), \"\\n\", _jsx(_components.p, {\n children: \"Routing into another campaign:\"\n }), \"\\n\", _jsxs(_components.ul, {\n children: [\"\\n\", _jsxs(_components.li, {\n children: [\"Use \", _jsx(_components.code, {\n children: \"add_to_another_campaign\"\n }), \" with a \", _jsx(_components.code, {\n children: \"campaignId\"\n }), \" to route the lead into an existing campaign in the same workspace.\"]\n }), \"\\n\", _jsxs(_components.li, {\n children: [\"The referenced \", _jsx(_components.code, {\n children: \"campaignId\"\n }), \" must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each \", _jsx(_components.code, {\n children: \"campaignId\"\n }), \" to the equivalent campaign in the destination workspace before posting.\"]\n }), \"\\n\", _jsx(_components.li, {\n children: \"Can be followed by additional steps in the same branch.\"\n }), \"\\n\"]\n }), \"\\n\", _jsxs(_components.p, {\n children: [\"For examples and a full step-by-step guide, see \", _jsx(_components.code, {\n children: \"/api-reference/campaigns-guide\"\n }), \" in the docs site.\"]\n }), \"\\n\", _jsxs(_components.blockquote, {\n children: [\"\\n\", _jsxs(_components.p, {\n children: [_jsx(_components.strong, {\n children: \"Required scope:\"\n }), \" \", _jsx(_components.code, {\n children: \"CAMPAIGNS_WRITE\"\n })]\n }), \"\\n\", _jsxs(_components.p, {\n children: [_jsx(_components.strong, {\n children: \"Rate limit:\"\n }), \" 30 requests per minute\"]\n }), \"\\n\"]\n })]\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){return _jsx(_Fragment,{})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Validate campaign","description":"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before `POST /v1/campaigns` when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign.\n\nThis endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy.\n\nCreate a draft campaign from the public `steps` format.\n\nHow to structure the request:\n- Start `steps` with the first action that should happen.\n- Keep adding the next action in the order it should run.\n- Use `condition` when you want to branch on lead data.\n- Use `linkedin_connection` with `waitForAcceptance` when you want to branch on connection acceptance after sending a connection request. Omit `waitForAcceptance` for a fire-and-forget connection request.\n- Use `linkedin_message` with `content`, `attachment`, or both. Attachment-only messages are valid.\n- Use `whatsapp_message` only when the workspace and sending identity are configured for WhatsApp.\n- Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches.\n\nSenders:\n- Pass `identityIds` (primary first) to send from several identities; the campaign spreads its contacts across them. `identityId` is the single-sender shorthand.\n- More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with `details.code` `MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED`.\n- Use `GET /v1/identities` to find identity IDs. `allowedEmails` only applies to single-sender campaigns.\n\nAI-generated messages (message library):\n- `email`, `linkedin_message`, `linkedin_inmail`, and `linkedin_connection` steps accept `aiMessageId` instead of manual `content` — the linked AI message writes a personalized message per contact at send time.\n- To reuse a prompt-library (\"message library\") prompt, list entries with `GET /v1/prompt-library/ai-messages` and pass the entry `id` as the step `aiMessageId`. The step gets its own copy automatically.\n- To reuse or create a workspace AI message, use `GET /v1/ai-variables/ai-messages` or `POST /v1/ai-variables/ai-messages` (full control over `prompt`, `toneId`, `model`, `outputLength`, and `splitMessages`), then pass its `id` as `aiMessageId`.\n- The AI message `channel` must match the step type (`EMAIL`, `LINKEDIN`, `LINKEDIN_INMAIL`, or `LINKEDIN_CONNECTION`).\n- Never combine `aiMessageId` with manual `content` or `subject` on the same step — for `email` and `linkedin_inmail` the AI message generates the subject too.\n\nEmail threading (Reply / New thread):\n- Set `thread` on an `email` step to pick how it is sent. `reply` sends it in the thread of the previous email, like the editor's Reply option, and needs no `subject`. `new_thread` starts a separate thread under the step's own `subject`.\n- When `thread` is omitted, a follow-up email without `subject` replies and one with `subject` is sent under that subject. Set `thread` explicitly whenever the choice matters, especially for `aiMessageId` follow-ups, which otherwise go out under their generated subject.\n\nBefore you send a request:\n- Send an ordered `steps` array that describes the campaign flow from first action to last action.\n- Use branch arrays on `condition` and `linkedin_connection` steps to describe branch-specific work.\n- Place shared follow-up steps after a branching step when both live branches should continue together.\n- `waitForAcceptance.unit` must be `days` and `waitForAcceptance.value` must be an integer greater than or equal to `1`.\n- Use `GET /v1/tasks/owners` first when your campaign includes `task` steps. If owners are returned, every `task` step must include `ownerId`.\n- Validation errors use full paths like `steps[0].onTrue[1].subject` so nested input issues are easy to fix.\n- Personalization placeholders use Enginy single-brace syntax such as `{firstName}`, `{companyName}`, and `{identity.name}`. Do not send Handlebars-style placeholders like `{{firstName}}`.\n- Placeholders must reference real fields — discover them with `GET /v1/contacts/fields`. Unknown placeholders are rejected with a 422. The company aliases `{company}`, `{companyName}`, and `{companyDomain}` are accepted and stored as the canonical company columns.\n- To modify an existing DRAFT campaign in place, prefer `PATCH /v1/campaign/{campaignId}` over creating a new one. To rewrite into a new campaign, start from the `steps` returned by `GET /v1/campaign/{campaignId}`. Either way the read-only `stepId`, `connectionId`, `nextConnectionId`, and `advanced` fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their `aiMessageId` binding.\n\nSupported `condition.type` values:\n- `has_linkedin_profile`: `onTrue` runs when the lead has a LinkedIn profile.\n- `has_professional_email`: `onTrue` runs when the lead has a professional email.\n- `is_already_connected`: `onTrue` runs when the lead is already a LinkedIn connection.\n- `has_been_contacted`: `onTrue` runs when the lead has already been contacted.\n- `has_whatsapp_account`: `onTrue` runs when the lead has a WhatsApp account.\n- `email_opened`: `onTrue` runs when the lead opens the preceding email within `waitFor`. Must follow an `email` step. Requires `waitFor` (`value`, `unit`).\n- `email_clicked`: `onTrue` runs when the lead clicks a link in the preceding email within `waitFor`. Must follow an `email` step. Requires `waitFor` (`value`, `unit`).\n- `linkedin_message_viewed`: `onTrue` runs when the lead views the preceding LinkedIn message within `waitFor`. Must follow a LinkedIn message step (`linkedin_message`, `linkedin_message_bundle`, `linkedin_voice_message`, or `linkedin_inmail`). Requires `waitFor` (`value`, `unit`).\n- `task_completed`: `onTrue` runs when the preceding `task` step is marked complete within `waitFor`. Must follow a `task` step. Requires `waitFor` (`value`, `unit`).\n- `connection_accepted`: `onTrue` runs when the lead accepts a previous LinkedIn connection request within `waitFor`. Requires `waitFor` (`value`, `unit`).\n- `lead_field`: `onTrue` runs when the contact `field` matches the `operator` (and `value`, where applicable). Use this to branch on any built-in column or custom field. Requires `field` and `operator`. `value` is required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`. Supported operators: `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `NOT_CONTAINS`, `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_OR_EQUALS`, `LESS_THAN_OR_EQUALS`, `IS_EMPTY`, `IS_NOT_EMPTY`.\n\nBranch fields:\n- `condition` uses `onTrue` and `onFalse`.\n- `linkedin_connection` uses `onAccepted` and `onNotAccepted` only when `waitForAcceptance` is set.\n- Use `[{ \"type\": \"end\" }]` when a branch should stop explicitly.\n\nRouting into another campaign:\n- Use `add_to_another_campaign` with a `campaignId` to route the lead into an existing campaign in the same workspace.\n- The referenced `campaignId` must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each `campaignId` to the equivalent campaign in the destination workspace before posting.\n- Can be followed by additional steps in the same branch.\n\nFor examples and a full step-by-step guide, see `/api-reference/campaigns-guide` in the docs site.\n\n > **Required scope:** `CAMPAIGNS_WRITE`\n >\n > **Rate limit:** 30 requests per minute","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/campaigns/validate","href":"/api-reference/campaigns/validate-campaign","autogeneratedByOpenApi":true}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[],"codeExamples":{}},"jsLinks":[],"openApiReferenceData":{"operation":{"summary":"Validate campaign","description":"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before `POST /v1/campaigns` when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign.\n\nThis endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy.\n\nCreate a draft campaign from the public `steps` format.\n\nHow to structure the request:\n- Start `steps` with the first action that should happen.\n- Keep adding the next action in the order it should run.\n- Use `condition` when you want to branch on lead data.\n- Use `linkedin_connection` with `waitForAcceptance` when you want to branch on connection acceptance after sending a connection request. Omit `waitForAcceptance` for a fire-and-forget connection request.\n- Use `linkedin_message` with `content`, `attachment`, or both. Attachment-only messages are valid.\n- Use `whatsapp_message` only when the workspace and sending identity are configured for WhatsApp.\n- Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches.\n\nSenders:\n- Pass `identityIds` (primary first) to send from several identities; the campaign spreads its contacts across them. `identityId` is the single-sender shorthand.\n- More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with `details.code` `MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED`.\n- Use `GET /v1/identities` to find identity IDs. `allowedEmails` only applies to single-sender campaigns.\n\nAI-generated messages (message library):\n- `email`, `linkedin_message`, `linkedin_inmail`, and `linkedin_connection` steps accept `aiMessageId` instead of manual `content` — the linked AI message writes a personalized message per contact at send time.\n- To reuse a prompt-library (\"message library\") prompt, list entries with `GET /v1/prompt-library/ai-messages` and pass the entry `id` as the step `aiMessageId`. The step gets its own copy automatically.\n- To reuse or create a workspace AI message, use `GET /v1/ai-variables/ai-messages` or `POST /v1/ai-variables/ai-messages` (full control over `prompt`, `toneId`, `model`, `outputLength`, and `splitMessages`), then pass its `id` as `aiMessageId`.\n- The AI message `channel` must match the step type (`EMAIL`, `LINKEDIN`, `LINKEDIN_INMAIL`, or `LINKEDIN_CONNECTION`).\n- Never combine `aiMessageId` with manual `content` or `subject` on the same step — for `email` and `linkedin_inmail` the AI message generates the subject too.\n\nEmail threading (Reply / New thread):\n- Set `thread` on an `email` step to pick how it is sent. `reply` sends it in the thread of the previous email, like the editor's Reply option, and needs no `subject`. `new_thread` starts a separate thread under the step's own `subject`.\n- When `thread` is omitted, a follow-up email without `subject` replies and one with `subject` is sent under that subject. Set `thread` explicitly whenever the choice matters, especially for `aiMessageId` follow-ups, which otherwise go out under their generated subject.\n\nBefore you send a request:\n- Send an ordered `steps` array that describes the campaign flow from first action to last action.\n- Use branch arrays on `condition` and `linkedin_connection` steps to describe branch-specific work.\n- Place shared follow-up steps after a branching step when both live branches should continue together.\n- `waitForAcceptance.unit` must be `days` and `waitForAcceptance.value` must be an integer greater than or equal to `1`.\n- Use `GET /v1/tasks/owners` first when your campaign includes `task` steps. If owners are returned, every `task` step must include `ownerId`.\n- Validation errors use full paths like `steps[0].onTrue[1].subject` so nested input issues are easy to fix.\n- Personalization placeholders use Enginy single-brace syntax such as `{firstName}`, `{companyName}`, and `{identity.name}`. Do not send Handlebars-style placeholders like `{{firstName}}`.\n- Placeholders must reference real fields — discover them with `GET /v1/contacts/fields`. Unknown placeholders are rejected with a 422. The company aliases `{company}`, `{companyName}`, and `{companyDomain}` are accepted and stored as the canonical company columns.\n- To modify an existing DRAFT campaign in place, prefer `PATCH /v1/campaign/{campaignId}` over creating a new one. To rewrite into a new campaign, start from the `steps` returned by `GET /v1/campaign/{campaignId}`. Either way the read-only `stepId`, `connectionId`, `nextConnectionId`, and `advanced` fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their `aiMessageId` binding.\n\nSupported `condition.type` values:\n- `has_linkedin_profile`: `onTrue` runs when the lead has a LinkedIn profile.\n- `has_professional_email`: `onTrue` runs when the lead has a professional email.\n- `is_already_connected`: `onTrue` runs when the lead is already a LinkedIn connection.\n- `has_been_contacted`: `onTrue` runs when the lead has already been contacted.\n- `has_whatsapp_account`: `onTrue` runs when the lead has a WhatsApp account.\n- `email_opened`: `onTrue` runs when the lead opens the preceding email within `waitFor`. Must follow an `email` step. Requires `waitFor` (`value`, `unit`).\n- `email_clicked`: `onTrue` runs when the lead clicks a link in the preceding email within `waitFor`. Must follow an `email` step. Requires `waitFor` (`value`, `unit`).\n- `linkedin_message_viewed`: `onTrue` runs when the lead views the preceding LinkedIn message within `waitFor`. Must follow a LinkedIn message step (`linkedin_message`, `linkedin_message_bundle`, `linkedin_voice_message`, or `linkedin_inmail`). Requires `waitFor` (`value`, `unit`).\n- `task_completed`: `onTrue` runs when the preceding `task` step is marked complete within `waitFor`. Must follow a `task` step. Requires `waitFor` (`value`, `unit`).\n- `connection_accepted`: `onTrue` runs when the lead accepts a previous LinkedIn connection request within `waitFor`. Requires `waitFor` (`value`, `unit`).\n- `lead_field`: `onTrue` runs when the contact `field` matches the `operator` (and `value`, where applicable). Use this to branch on any built-in column or custom field. Requires `field` and `operator`. `value` is required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`. Supported operators: `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `NOT_CONTAINS`, `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_OR_EQUALS`, `LESS_THAN_OR_EQUALS`, `IS_EMPTY`, `IS_NOT_EMPTY`.\n\nBranch fields:\n- `condition` uses `onTrue` and `onFalse`.\n- `linkedin_connection` uses `onAccepted` and `onNotAccepted` only when `waitForAcceptance` is set.\n- Use `[{ \"type\": \"end\" }]` when a branch should stop explicitly.\n\nRouting into another campaign:\n- Use `add_to_another_campaign` with a `campaignId` to route the lead into an existing campaign in the same workspace.\n- The referenced `campaignId` must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each `campaignId` to the equivalent campaign in the destination workspace before posting.\n- Can be followed by additional steps in the same branch.\n\nFor examples and a full step-by-step guide, see `/api-reference/campaigns-guide` in the docs site.\n\n > **Required scope:** `CAMPAIGNS_WRITE`\n >\n > **Rate limit:** 30 requests per minute","tags":["Campaigns"],"security":["662a52a8-3164-45b1-a27a-868a50479843"],"requestBody":"4f8f11ab-92d0-47eb-a95c-f168a179f7c9","responses":{"200":"55beff26-5cf8-4ce1-bae5-355fbf7ddec7","400":"b1a849e1-566d-4807-a1ef-60781ddb19ac","422":"d19961b1-1a8e-44ed-8379-e180b5f8af38"},"method":"post","type":"path","servers":["0583bc01-7d89-47ba-9860-852cd05faae2"],"path":"/v1/campaigns/validate","baseUrlOptions":["https://openapi.enginy.ai"],"title":"Validate campaign","codeSamples":[],"requiredOnlyExamples":false,"requestExampleType":"generated","responseExampleType":"generated"},"dependencies":{"parameters":{},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Campaign name.","isRequired":true,"uniqueKey":"name","typeLabel":"string"},"steps":{"type":"array","items":{"oneOf":[{"type":"object","title":"email","required":["type"],"properties":{"type":{"type":"string","enum":["email"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.0.type","typeLabel":"enum\u003cstring>"},"subject":{"type":"string","description":"Email subject. Required with manual `content` for the first email on every live path; follow-up emails may pass an empty string (or omit it) to reply in the existing thread. Omit when `aiMessageId` is set — the AI message generates it.","uniqueKey":"steps.items.[INDEX].oneOf.0.subject","typeLabel":"string"},"content":{"type":"string","minLength":1,"description":"Manual email body. Required unless `aiMessageId` is provided.","uniqueKey":"steps.items.[INDEX].oneOf.0.content","typeLabel":"string"},"aiMessageId":{"type":"string","minLength":1,"description":"ID of an AI message that generates this step's message per contact at send time. Accepts a workspace-owned AI message (see `GET /v1/ai-variables/ai-messages`, or create one with `POST /v1/ai-variables/ai-messages` to control prompt, tone, model, and output length) or a public prompt-library entry (see `GET /v1/prompt-library/ai-messages`); either way the campaign step gets its own copy. The AI message channel must match the step type. Mutually exclusive with manual `content`, `subject`, and attachment fields. Requires the AI variable split to be enabled for the workspace.","uniqueKey":"steps.items.[INDEX].oneOf.0.aiMessageId","typeLabel":"string"},"thread":{"type":"string","enum":["reply","new_thread"],"description":"Reply / New thread choice, as in the campaign editor. `reply` sends this email in the thread of the previous email; its own `subject` is then only used for a contact who reaches the step with no earlier email. `new_thread` starts a separate thread under this step's `subject`, which is required unless `aiMessageId` is set. When omitted, a follow-up without `subject` replies and one with `subject` is sent under that subject.","uniqueKey":"steps.items.[INDEX].oneOf.0.thread","typeLabel":"enum\u003cstring>"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.0.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.0.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.0.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.0","typeLabel":"email · object"},{"type":"object","title":"linkedin_message","required":["type"],"properties":{"type":{"type":"string","enum":["linkedin_message"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.1.type","typeLabel":"enum\u003cstring>"},"aiMessageId":{"type":"string","minLength":1,"description":"ID of an AI message that generates this step's message per contact at send time. Accepts a workspace-owned AI message (see `GET /v1/ai-variables/ai-messages`, or create one with `POST /v1/ai-variables/ai-messages` to control prompt, tone, model, and output length) or a public prompt-library entry (see `GET /v1/prompt-library/ai-messages`); either way the campaign step gets its own copy. The AI message channel must match the step type. Mutually exclusive with manual `content`, `subject`, and attachment fields. Requires the AI variable split to be enabled for the workspace.","uniqueKey":"steps.items.[INDEX].oneOf.1.aiMessageId","typeLabel":"string"},"content":{"type":"string","minLength":1,"description":"LinkedIn message body. Required unless `attachment` or `aiMessageId` is provided.","uniqueKey":"steps.items.[INDEX].oneOf.1.content","typeLabel":"string"},"attachment":{"type":"string","minLength":1,"description":"Optional URL or storage key of the file to send alongside the message.","uniqueKey":"steps.items.[INDEX].oneOf.1.attachment","typeLabel":"string"},"attachmentName":{"type":"string","minLength":1,"description":"Optional display name for the attachment.","uniqueKey":"steps.items.[INDEX].oneOf.1.attachmentName","typeLabel":"string"},"contentType":{"type":"string","enum":["AUDIO","IMAGE","FILE","VIDEO"],"description":"Attachment kind. Recommended when `attachment` is set. Use `VIDEO` for video files.","uniqueKey":"steps.items.[INDEX].oneOf.1.contentType","typeLabel":"enum\u003cstring>"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.1.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.1.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.1.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.1","typeLabel":"linkedin_message · object"},{"type":"object","title":"linkedin_message_bundle","required":["type","messages"],"properties":{"type":{"type":"string","enum":["linkedin_message_bundle"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.2.type","typeLabel":"enum\u003cstring>"},"messages":{"type":"array","items":{"type":"object","properties":{"content":{"type":"string","minLength":1,"description":"Text body of the bundle message.","uniqueKey":"steps.items.[INDEX].oneOf.2.messages.items.[INDEX].content","typeLabel":"string"},"attachment":{"type":"string","minLength":1,"description":"URL or storage key of the file to send.","uniqueKey":"steps.items.[INDEX].oneOf.2.messages.items.[INDEX].attachment","typeLabel":"string"},"attachmentName":{"type":"string","minLength":1,"description":"Display name for the attachment.","uniqueKey":"steps.items.[INDEX].oneOf.2.messages.items.[INDEX].attachmentName","typeLabel":"string"},"contentType":{"type":"string","enum":["AUDIO","IMAGE","FILE","VIDEO"],"description":"Attachment kind. Recommended when `attachment` is set. Use `VIDEO` for video files.","uniqueKey":"steps.items.[INDEX].oneOf.2.messages.items.[INDEX].contentType","typeLabel":"enum\u003cstring>"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.2.messages.items.[INDEX]","typeLabel":"object"},"minItems":1,"description":"Ordered list of messages sent back-to-back as a single bundle.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.2.messages","typeLabel":"object[]"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.2.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.2.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.2.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.2","typeLabel":"linkedin_message_bundle · object"},{"type":"object","title":"linkedin_voice_message","required":["type"],"properties":{"type":{"type":"string","enum":["linkedin_voice_message"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.3.type","typeLabel":"enum\u003cstring>"},"content":{"type":"string","minLength":1,"description":"Text body that will be synthesized to speech via the configured `voiceId`.","uniqueKey":"steps.items.[INDEX].oneOf.3.content","typeLabel":"string"},"voiceId":{"type":"string","minLength":1,"description":"ID of a system voice or an identity-cloned voice. Use `GET /v1/voices` to list valid voice IDs.","uniqueKey":"steps.items.[INDEX].oneOf.3.voiceId","typeLabel":"string"},"attachment":{"type":"string","minLength":1,"description":"URL or storage key for a pre-recorded audio message.","uniqueKey":"steps.items.[INDEX].oneOf.3.attachment","typeLabel":"string"},"attachmentName":{"type":"string","minLength":1,"description":"Optional display name for the audio file.","uniqueKey":"steps.items.[INDEX].oneOf.3.attachmentName","typeLabel":"string"},"contentType":{"type":"string","enum":["AUDIO"],"description":"Set to `AUDIO` when `attachment` is provided.","uniqueKey":"steps.items.[INDEX].oneOf.3.contentType","typeLabel":"enum\u003cstring>"},"voiceSettings":{"type":"object","properties":{"voiceSpeed":{"type":"number","minimum":0.5,"maximum":2,"description":"Speech rate multiplier. Range 0.5–2.0. Defaults are applied server-side when omitted.","uniqueKey":"steps.items.[INDEX].oneOf.3.voiceSettings.voiceSpeed","typeLabel":"number"},"voiceStability":{"type":"number","minimum":0,"maximum":100,"description":"Voice stability percentage (0–100). Higher is steadier, lower is more expressive.","uniqueKey":"steps.items.[INDEX].oneOf.3.voiceSettings.voiceStability","typeLabel":"number"},"backgroundNoise":{"type":"string","minLength":1,"description":"Optional background noise preset (e.g. `none`, `office`). Server defaults to `none`.","uniqueKey":"steps.items.[INDEX].oneOf.3.voiceSettings.backgroundNoise","typeLabel":"string"},"backgroundVolume":{"type":"number","minimum":0,"maximum":100,"description":"Background noise volume (0–100). Ignored when `backgroundNoise` is `none`.","uniqueKey":"steps.items.[INDEX].oneOf.3.voiceSettings.backgroundVolume","typeLabel":"number"}},"additionalProperties":false,"description":"Optional fine-tuning of the synthesized voice and background audio.","uniqueKey":"steps.items.[INDEX].oneOf.3.voiceSettings","typeLabel":"object"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.3.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.3.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.3.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.3","typeLabel":"linkedin_voice_message · object"},{"type":"object","title":"linkedin_inmail","required":["type"],"properties":{"type":{"type":"string","enum":["linkedin_inmail"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.4.type","typeLabel":"enum\u003cstring>"},"subject":{"type":"string","minLength":1,"description":"InMail subject. Required with manual `content`. Omit when `aiMessageId` is set.","uniqueKey":"steps.items.[INDEX].oneOf.4.subject","typeLabel":"string"},"content":{"type":"string","minLength":1,"description":"Manual InMail body. Required unless `aiMessageId` is provided.","uniqueKey":"steps.items.[INDEX].oneOf.4.content","typeLabel":"string"},"aiMessageId":{"type":"string","minLength":1,"description":"ID of an AI message that generates this step's message per contact at send time. Accepts a workspace-owned AI message (see `GET /v1/ai-variables/ai-messages`, or create one with `POST /v1/ai-variables/ai-messages` to control prompt, tone, model, and output length) or a public prompt-library entry (see `GET /v1/prompt-library/ai-messages`); either way the campaign step gets its own copy. The AI message channel must match the step type. Mutually exclusive with manual `content`, `subject`, and attachment fields. Requires the AI variable split to be enabled for the workspace.","uniqueKey":"steps.items.[INDEX].oneOf.4.aiMessageId","typeLabel":"string"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.4.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.4.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.4.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.4","typeLabel":"linkedin_inmail · object"},{"type":"object","title":"linkedin_visit_profile","required":["type"],"properties":{"type":{"type":"string","enum":["linkedin_visit_profile"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.5.type","typeLabel":"enum\u003cstring>"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.5.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.5.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.5.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.5","typeLabel":"linkedin_visit_profile · object"},{"type":"object","title":"linkedin_like_last_post","required":["type"],"properties":{"type":{"type":"string","enum":["linkedin_like_last_post"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.6.type","typeLabel":"enum\u003cstring>"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.6.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.6.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.6.delay","typeLabel":"object"},"reactionType":{"type":"string","minLength":1,"uniqueKey":"steps.items.[INDEX].oneOf.6.reactionType","typeLabel":"string"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.6","typeLabel":"linkedin_like_last_post · object"},{"type":"object","title":"whatsapp_message","required":["type","content"],"properties":{"type":{"type":"string","enum":["whatsapp_message"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.7.type","typeLabel":"enum\u003cstring>"},"content":{"type":"string","minLength":1,"description":"WhatsApp message body. Requires WhatsApp support on the workspace and identity.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.7.content","typeLabel":"string"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.7.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.7.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.7.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.7","typeLabel":"whatsapp_message · object"},{"type":"object","title":"condition","required":["type","condition"],"properties":{"type":{"type":"string","enum":["condition"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.type","typeLabel":"enum\u003cstring>"},"condition":{"oneOf":[{"type":"object","title":"has_linkedin_profile","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type"],"properties":{"type":{"type":"string","enum":["has_linkedin_profile"],"description":"Lead has a LinkedIn profile.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.0.type","typeLabel":"enum\u003cstring>"},"field":{"type":"string","enum":["linkedinProfileUrl","linkedInProfileId"],"description":"Stored lead field to check. Defaults to `linkedinProfileUrl`.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.0.field","typeLabel":"enum\u003cstring>"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.0","typeLabel":"has_linkedin_profile · object"},{"type":"object","title":"has_professional_email","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type"],"properties":{"type":{"type":"string","enum":["has_professional_email"],"description":"Lead has a professional email.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.1.type","typeLabel":"enum\u003cstring>"},"verificationStatuses":{"type":"array","items":{"type":"string","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.1.verificationStatuses.items.[INDEX]","typeLabel":"string"},"minItems":1,"description":"Optional email verification statuses to include when matching. When provided it must be non-empty; omit it to match any status.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.1.verificationStatuses","typeLabel":"string[]"},"includeEmpty":{"type":"boolean","description":"Whether empty professional email values match.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.1.includeEmpty","typeLabel":"boolean"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.1","typeLabel":"has_professional_email · object"},{"type":"object","title":"is_already_connected","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type"],"properties":{"type":{"type":"string","enum":["is_already_connected"],"description":"Lead is already a LinkedIn connection.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.2.type","typeLabel":"enum\u003cstring>"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.2","typeLabel":"is_already_connected · object"},{"type":"object","title":"has_been_contacted","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type"],"properties":{"type":{"type":"string","enum":["has_been_contacted"],"description":"Lead has already been contacted.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.3.type","typeLabel":"enum\u003cstring>"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.3","typeLabel":"has_been_contacted · object"},{"type":"object","title":"has_whatsapp_account","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type"],"properties":{"type":{"type":"string","enum":["has_whatsapp_account"],"description":"Lead has a WhatsApp account.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.4.type","typeLabel":"enum\u003cstring>"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.4","typeLabel":"has_whatsapp_account · object"},{"type":"object","title":"email_opened","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type","waitFor"],"properties":{"type":{"type":"string","enum":["email_opened"],"description":"Branch on whether the preceding email is opened within `waitFor`.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.5.type","typeLabel":"enum\u003cstring>"},"waitFor":{"type":"object","properties":{"value":{"type":"number","exclusiveMinimum":0,"description":"Wait amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.5.waitFor.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Wait unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.5.waitFor.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"description":"How long to wait for the lead to open the preceding email. Required.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.5.waitFor","typeLabel":"object"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.5","typeLabel":"email_opened · object"},{"type":"object","title":"email_clicked","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type","waitFor"],"properties":{"type":{"type":"string","enum":["email_clicked"],"description":"Branch on whether a link in the preceding email is clicked within `waitFor`.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.6.type","typeLabel":"enum\u003cstring>"},"waitFor":{"type":"object","properties":{"value":{"type":"number","exclusiveMinimum":0,"description":"Wait amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.6.waitFor.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Wait unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.6.waitFor.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"description":"How long to wait for the lead to click a link in the preceding email. Required.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.6.waitFor","typeLabel":"object"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.6","typeLabel":"email_clicked · object"},{"type":"object","title":"linkedin_message_viewed","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type","waitFor"],"properties":{"type":{"type":"string","enum":["linkedin_message_viewed"],"description":"Branch on whether the preceding LinkedIn message is viewed within `waitFor`.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.7.type","typeLabel":"enum\u003cstring>"},"waitFor":{"type":"object","properties":{"value":{"type":"number","exclusiveMinimum":0,"description":"Wait amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.7.waitFor.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Wait unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.7.waitFor.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"description":"How long to wait for the lead to view the preceding LinkedIn message. Required.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.7.waitFor","typeLabel":"object"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.7","typeLabel":"linkedin_message_viewed · object"},{"type":"object","title":"task_completed","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type","waitFor"],"properties":{"type":{"type":"string","enum":["task_completed"],"description":"Branch on whether the preceding task is marked complete within `waitFor`.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.8.type","typeLabel":"enum\u003cstring>"},"waitFor":{"type":"object","properties":{"value":{"type":"number","exclusiveMinimum":0,"description":"Wait amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.8.waitFor.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Wait unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.8.waitFor.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"description":"How long to wait for the task to be marked complete. Required.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.8.waitFor","typeLabel":"object"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.8","typeLabel":"task_completed · object"},{"type":"object","title":"connection_accepted","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type","waitFor"],"properties":{"type":{"type":"string","enum":["connection_accepted"],"description":"Branch on whether the lead accepts a previous LinkedIn connection request.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.9.type","typeLabel":"enum\u003cstring>"},"waitFor":{"type":"object","properties":{"value":{"type":"number","exclusiveMinimum":0,"description":"Wait amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.9.waitFor.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Wait unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.9.waitFor.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"description":"How long to wait for the lead to accept the previous LinkedIn connection request. Required.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.9.waitFor","typeLabel":"object"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.9","typeLabel":"connection_accepted · object"},{"type":"object","title":"lead_field","description":"Condition evaluated at this point in the sequence. Supported `condition.type` values are `has_linkedin_profile`, `has_professional_email`, `is_already_connected`, `has_been_contacted`, `has_whatsapp_account`, `email_opened`, `email_clicked`, `linkedin_message_viewed`, `task_completed`, `connection_accepted`, and `lead_field`. The email, LinkedIn-message-viewed, task, and connection-accepted variants require a `waitFor` object with `value` and `unit`. The `lead_field` variant requires `field` and `operator`, and a `value` for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","required":["type","field","operator"],"properties":{"type":{"type":"string","enum":["lead_field"],"description":"Branch on a lead field value.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.type","typeLabel":"enum\u003cstring>"},"field":{"type":"string","minLength":1,"description":"Built-in column name (e.g. `professionalEmail`, `mobilePhone`) or custom field name.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.field","typeLabel":"string"},"operator":{"type":"string","enum":["EQUALS","NOT_EQUALS","CONTAINS","NOT_CONTAINS","GREATER_THAN","LESS_THAN","GREATER_THAN_OR_EQUALS","LESS_THAN_OR_EQUALS","IS_EMPTY","IS_NOT_EMPTY"],"description":"Comparison operator. `IS_EMPTY` and `IS_NOT_EMPTY` ignore `value`; all others require it.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.operator","typeLabel":"enum\u003cstring>"},"value":{"oneOf":[{"type":"string","description":"Comparison value. Required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.0","typeLabel":"string"},{"type":"number","description":"Comparison value. Required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.1","typeLabel":"number"},{"type":"boolean","description":"Comparison value. Required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.2","typeLabel":"boolean"},{"type":"array","items":{"type":"string","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.3.items.[INDEX]","typeLabel":"string"},"description":"Comparison value. Required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.3","typeLabel":"string[]"},{"type":"array","items":{"type":"number","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.4.items.[INDEX]","typeLabel":"number"},"description":"Comparison value. Required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.4","typeLabel":"number[]"},{"type":"array","items":{"type":"boolean","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.5.items.[INDEX]","typeLabel":"boolean"},"description":"Comparison value. Required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`.","uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value.oneOf.5","typeLabel":"boolean[]"}],"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10.value","typeLabel":"string | number | boolean | string[] | number[] | boolean[]"}},"additionalProperties":false,"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition.oneOf.10","typeLabel":"lead_field · object"}],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.condition","typeLabel":"has_linkedin_profile · object | has_professional_email · object | is_already_connected · object | has_been_contacted · object | has_whatsapp_account · object | email_opened · object | email_clicked · object | linkedin_message_viewed · object | task_completed · object | connection_accepted · object | lead_field · object"},"onTrue":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"Nested branch step type.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.onTrue.items.[INDEX].type","typeLabel":"string"}},"required":["type"],"additionalProperties":{"oneOf":[{}],"uniqueKey":"steps.items.[INDEX].oneOf.8.onTrue.items.[INDEX].additionalProperties","typeLabel":"any"},"description":"Nested branch step. Use the same step payload shape as top-level `steps` items.","uniqueKey":"steps.items.[INDEX].oneOf.8.onTrue.items.[INDEX]","typeLabel":"object"},"minItems":1,"description":"Steps to run when the condition matches. If provided, it must contain at least one step. Use `{ \"type\": \"end\" }` to stop that branch explicitly.","uniqueKey":"steps.items.[INDEX].oneOf.8.onTrue","typeLabel":"object[]"},"onFalse":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"Nested branch step type.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.8.onFalse.items.[INDEX].type","typeLabel":"string"}},"required":["type"],"additionalProperties":{"oneOf":[{}],"uniqueKey":"steps.items.[INDEX].oneOf.8.onFalse.items.[INDEX].additionalProperties","typeLabel":"any"},"description":"Nested branch step. Use the same step payload shape as top-level `steps` items.","uniqueKey":"steps.items.[INDEX].oneOf.8.onFalse.items.[INDEX]","typeLabel":"object"},"minItems":1,"description":"Steps to run when the condition does not match. If provided, it must contain at least one step. Use `{ \"type\": \"end\" }` to stop that branch explicitly.","uniqueKey":"steps.items.[INDEX].oneOf.8.onFalse","typeLabel":"object[]"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.8","typeLabel":"condition · object"},{"type":"object","title":"linkedin_connection","required":["type"],"properties":{"type":{"type":"string","enum":["linkedin_connection"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.9.type","typeLabel":"enum\u003cstring>"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.9.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.9.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.9.delay","typeLabel":"object"},"content":{"type":"string","minLength":1,"description":"Manual connection note. Mutually exclusive with `aiMessageId`.","uniqueKey":"steps.items.[INDEX].oneOf.9.content","typeLabel":"string"},"aiMessageId":{"type":"string","minLength":1,"description":"ID of an AI message that generates this step's message per contact at send time. Accepts a workspace-owned AI message (see `GET /v1/ai-variables/ai-messages`, or create one with `POST /v1/ai-variables/ai-messages` to control prompt, tone, model, and output length) or a public prompt-library entry (see `GET /v1/prompt-library/ai-messages`); either way the campaign step gets its own copy. The AI message channel must match the step type. Mutually exclusive with manual `content`, `subject`, and attachment fields. Requires the AI variable split to be enabled for the workspace.","uniqueKey":"steps.items.[INDEX].oneOf.9.aiMessageId","typeLabel":"string"},"waitForAcceptance":{"type":"object","properties":{"value":{"type":"integer","minimum":1,"description":"whole number of days to wait. Minimum 1 day.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.9.waitForAcceptance.value","typeLabel":"integer"},"unit":{"type":"string","enum":["days"],"description":"Connection wait unit. Must be `days`.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.9.waitForAcceptance.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"description":"Optional. Provide an acceptance window to branch the flow on whether the lead accepted the request. Omit to send the request without an inline acceptance check; the campaign continues with whatever follows.","uniqueKey":"steps.items.[INDEX].oneOf.9.waitForAcceptance","typeLabel":"object"},"onAccepted":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"Nested branch step type.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.9.onAccepted.items.[INDEX].type","typeLabel":"string"}},"required":["type"],"additionalProperties":{"oneOf":[{}],"uniqueKey":"steps.items.[INDEX].oneOf.9.onAccepted.items.[INDEX].additionalProperties","typeLabel":"any"},"description":"Nested branch step. Use the same step payload shape as top-level `steps` items.","uniqueKey":"steps.items.[INDEX].oneOf.9.onAccepted.items.[INDEX]","typeLabel":"object"},"minItems":1,"description":"Accepted branch steps. Only valid when `waitForAcceptance` is set. If provided, it must contain at least one step. Use `{ \"type\": \"end\" }` to stop that branch explicitly.","uniqueKey":"steps.items.[INDEX].oneOf.9.onAccepted","typeLabel":"object[]"},"onNotAccepted":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","description":"Nested branch step type.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.9.onNotAccepted.items.[INDEX].type","typeLabel":"string"}},"required":["type"],"additionalProperties":{"oneOf":[{}],"uniqueKey":"steps.items.[INDEX].oneOf.9.onNotAccepted.items.[INDEX].additionalProperties","typeLabel":"any"},"description":"Nested branch step. Use the same step payload shape as top-level `steps` items.","uniqueKey":"steps.items.[INDEX].oneOf.9.onNotAccepted.items.[INDEX]","typeLabel":"object"},"minItems":1,"description":"Not-accepted branch steps. Only valid when `waitForAcceptance` is set. If provided, it must contain at least one step. Use `{ \"type\": \"end\" }` to stop that branch explicitly.","uniqueKey":"steps.items.[INDEX].oneOf.9.onNotAccepted","typeLabel":"object[]"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.9","typeLabel":"linkedin_connection · object"},{"type":"object","title":"task","required":["type","taskType","title"],"properties":{"type":{"type":"string","enum":["task"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.10.type","typeLabel":"enum\u003cstring>"},"taskType":{"type":"string","minLength":1,"description":"Task type. Allowed values depend on the connected CRM. For HubSpot use one of: CALL, EMAIL, or TODO. Do not invent variants such as \"COLD_CALL\" — use the closest standard value (e.g. \"CALL\").","example":"CALL","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.10.taskType","typeLabel":"string"},"title":{"type":"string","minLength":1,"description":"Required task title shown in the UI.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.10.title","typeLabel":"string"},"note":{"type":"string","minLength":1,"description":"Optional task note shown in the UI.","uniqueKey":"steps.items.[INDEX].oneOf.10.note","typeLabel":"string"},"ownerId":{"oneOf":[{"type":"string","minLength":1,"description":"Required when `GET /v1/tasks/owners` returns one or more owners.","uniqueKey":"steps.items.[INDEX].oneOf.10.ownerId.oneOf.0","typeLabel":"string"},{"type":"null","minLength":1,"description":"Required when `GET /v1/tasks/owners` returns one or more owners.","uniqueKey":"steps.items.[INDEX].oneOf.10.ownerId.oneOf.1","typeLabel":"null"}],"uniqueKey":"steps.items.[INDEX].oneOf.10.ownerId","typeLabel":"string | null"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.10.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.10.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.10.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.10","typeLabel":"task · object"},{"type":"object","title":"add_to_another_campaign","required":["type","campaignId"],"properties":{"type":{"type":"string","enum":["add_to_another_campaign"],"description":"Routes the lead into another campaign owned by the same workspace, then ends this branch.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.11.type","typeLabel":"enum\u003cstring>"},"campaignId":{"type":"integer","exclusiveMinimum":0,"description":"ID of a campaign owned by the same client as the API key making the request.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.11.campaignId","typeLabel":"integer"},"delay":{"type":"object","properties":{"value":{"type":"number","minimum":0,"description":"Delay amount.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.11.delay.value","typeLabel":"number"},"unit":{"type":"string","enum":["seconds","minutes","hours","days"],"description":"Delay unit.","isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.11.delay.unit","typeLabel":"enum\u003cstring>"}},"required":["value","unit"],"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.11.delay","typeLabel":"object"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.11","typeLabel":"add_to_another_campaign · object"},{"type":"object","title":"end","required":["type"],"properties":{"type":{"type":"string","enum":["end"],"isRequired":true,"uniqueKey":"steps.items.[INDEX].oneOf.12.type","typeLabel":"enum\u003cstring>"}},"additionalProperties":false,"uniqueKey":"steps.items.[INDEX].oneOf.12","typeLabel":"end · object"}],"uniqueKey":"steps.items.[INDEX]","typeLabel":"email · object | linkedin_message · object | linkedin_message_bundle · object | linkedin_voice_message · object | linkedin_inmail · object | linkedin_visit_profile · object | linkedin_like_last_post · object | whatsapp_message · object | condition · object | linkedin_connection · object | task · object | add_to_another_campaign · object | end · object"},"minItems":1,"description":"Ordered campaign steps. Use branch arrays like `onTrue`, `onFalse`, `onAccepted`, and `onNotAccepted` for branching. GET includes saved manual template content, with HTML email bodies. Steps PATCH rejects dynamic template-email bodies with 422 before replacement. When resubmitting steps, the read-only `stepId`, `connectionId`, `nextConnectionId`, and `advanced` fields are accepted and ignored.","isRequired":true,"uniqueKey":"steps","typeLabel":"(email · object | linkedin_message · object | linkedin_message_bundle · object | linkedin_voice_message · object | linkedin_inmail · object | linkedin_visit_profile · object | linkedin_like_last_post · object | whatsapp_message · object | condition · object | linkedin_connection · object | task · object | add_to_another_campaign · object | end · object)[]"},"description":{"oneOf":[{"type":"string","description":"Optional campaign description.","uniqueKey":"description.oneOf.0","typeLabel":"string"},{"type":"null","description":"Optional campaign description.","uniqueKey":"description.oneOf.1","typeLabel":"null"}],"uniqueKey":"description","typeLabel":"string | null"},"identityId":{"oneOf":[{"type":"integer","exclusiveMinimum":0,"description":"Optional sender identity ID — shorthand for a single-sender `identityIds: [identityId]`. A sender is required later when activating the campaign.","uniqueKey":"identityId.oneOf.0","typeLabel":"integer"},{"type":"null","exclusiveMinimum":0,"description":"Optional sender identity ID — shorthand for a single-sender `identityIds: [identityId]`. A sender is required later when activating the campaign.","uniqueKey":"identityId.oneOf.1","typeLabel":"null"}],"uniqueKey":"identityId","typeLabel":"integer | null"},"identityIds":{"type":"array","items":{"type":"integer","exclusiveMinimum":0,"uniqueKey":"identityIds.items.[INDEX]","typeLabel":"integer"},"maxItems":100,"description":"Sender identity IDs, primary first (repeats are ignored). The campaign spreads its contacts across these senders. More than one sender requires the multi-identity campaigns beta on the workspace. When `identityId` is also sent it must equal the first entry.","uniqueKey":"identityIds","typeLabel":"integer[]"},"shouldAutomaticallySend":{"type":"boolean","uniqueKey":"shouldAutomaticallySend","typeLabel":"boolean"},"shouldSendTrackingPixel":{"type":"boolean","uniqueKey":"shouldSendTrackingPixel","typeLabel":"boolean"},"shouldTrackEmailLinks":{"type":"boolean","uniqueKey":"shouldTrackEmailLinks","typeLabel":"boolean"},"excludeContactedLeads":{"type":"boolean","uniqueKey":"excludeContactedLeads","typeLabel":"boolean"},"allowedEmails":{"type":"array","items":{"type":"string","uniqueKey":"allowedEmails.items.[INDEX]","typeLabel":"string"},"description":"Allowed sender email addresses for this campaign. Only for single-sender campaigns: rejected together with more than one sender identity.","uniqueKey":"allowedEmails","typeLabel":"string[]"}},"required":["name","steps"],"additionalProperties":false,"uniqueKey":"","typeLabel":"object"},"examples":{"conditionFirst":{"title":"conditionFirst","value":{"name":"Email or fallback task","identityId":323,"steps":[{"type":"condition","condition":{"type":"has_professional_email"},"onTrue":[{"type":"email","subject":"Great to connect","content":"Sharing a quick overview."}],"onFalse":[{"type":"task","taskType":"TODO","title":"Find email first","ownerId":"owner-1"}]}]}},"aiMessageSteps":{"title":"aiMessageSteps","value":{"name":"AI-personalized outreach","identityId":323,"steps":[{"type":"email","aiMessageId":"4f8a7c2e-1b6d-4e5f-9a3c-8d2b7f6e5a41"},{"type":"linkedin_message","aiMessageId":"9c1d3e5f-7a8b-4c2d-b6e4-0f9a8b7c6d5e","delay":{"value":2,"unit":"days"}}]}},"multipleSenders":{"title":"multipleSenders","value":{"name":"Team LinkedIn outreach","identityIds":[323,324,325],"steps":[{"type":"linkedin_connection","waitForAcceptance":{"value":5,"unit":"days"},"onAccepted":[{"type":"linkedin_message","content":"Thanks for connecting, {firstName}."}],"onNotAccepted":[{"type":"end"}]}]}},"linkedinAcceptance":{"title":"linkedinAcceptance","value":{"name":"LinkedIn acceptance flow","identityId":323,"steps":[{"type":"linkedin_connection","delay":{"value":1,"unit":"days"},"waitForAcceptance":{"value":5,"unit":"days"},"onAccepted":[{"type":"linkedin_message","content":"Thanks for accepting.","delay":{"value":2,"unit":"hours"}}],"onNotAccepted":[{"type":"task","taskType":"TODO","title":"Review no-response path","ownerId":"owner-1"}]},{"type":"email","subject":"Following up","content":"Continuing after the LinkedIn decision.","delay":{"value":1,"unit":"days"}}]}},"voiceMessageWithVideoFollowup":{"title":"voiceMessageWithVideoFollowup","value":{"name":"Voice + video outreach","identityId":323,"steps":[{"type":"linkedin_voice_message","content":"Hey {firstName}, sharing a quick voice intro about {companyName}.","voiceId":"bIHbv24MWmeRgasZH58o","voiceSettings":{"voiceSpeed":1,"voiceStability":80,"backgroundNoise":"none","backgroundVolume":25}},{"type":"linkedin_message","content":"Following up with the demo video.","attachment":"https://example.com/uploads/demo.mp4","attachmentName":"demo.mp4","contentType":"VIDEO","delay":{"value":1,"unit":"days"}}]}},"taskRoutingFallbacks":{"title":"taskRoutingFallbacks","value":{"name":"Task routing fallback flow","identityId":323,"steps":[{"type":"condition","condition":{"type":"has_professional_email"},"onTrue":[{"type":"email","subject":"Initial outreach","content":"Opening with email when contact data is ready."},{"type":"linkedin_connection","waitForAcceptance":{"value":4,"unit":"days"},"onAccepted":[{"type":"task","taskType":"CALL","title":"Call after positive signal","note":"They accepted the connection and saw the first email.","ownerId":"owner-1","delay":{"value":1,"unit":"days"}}],"onNotAccepted":[{"type":"task","taskType":"TODO","title":"Review cold branch manually","ownerId":"owner-2"}]}],"onFalse":[{"type":"task","taskType":"TODO","title":"Find email before launching sequence","ownerId":"owner-3"}]}]}}},"exampleType":"examples"}},"exampleType":"examples"},"responses":{"55beff26-5cf8-4ce1-bae5-355fbf7ddec7":{"description":"Campaign payload is valid against the public simplified schema","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["success"],"isRequired":true,"uniqueKey":"status","typeLabel":"enum\u003cstring>"},"message":{"type":"string","isRequired":true,"uniqueKey":"message","typeLabel":"string"},"data":{"type":"object","properties":{"isValid":{"type":"boolean","enum":[true],"description":"Always true for successful validation responses.","isRequired":true,"uniqueKey":"data.isValid","typeLabel":"enum\u003cboolean>"},"warnings":{"type":"array","items":{"type":"string","uniqueKey":"data.warnings.items.[INDEX]","typeLabel":"string"},"description":"Non-blocking sequence warnings returned by shared campaign validation.","isRequired":true,"uniqueKey":"data.warnings","typeLabel":"string[]"}},"required":["isValid","warnings"],"isRequired":true,"uniqueKey":"data","typeLabel":"object"}},"required":["status","message","data"],"uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"status":"success","message":"\u003cstring>","data":{"isValid":true,"warnings":["\u003cstring>"]}}}},"exampleType":"generated"}},"exampleType":"generated"},"b1a849e1-566d-4807-a1ef-60781ddb19ac":{"description":"Bad request - the campaign payload does not match the public simplified schema.\nCommon causes:\n- A referenced identity (`identityId` / `identityIds`) does not exist in this workspace, is deleted, or is not one you may send as.\n- `identityId` and `identityIds` were both sent but `identityId` is not `identityIds[0]`.\n- More than one sender identity while the multi-identity campaigns beta is off for the workspace (`details.code` `MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED`): send a single identity, or ask a workspace admin to enable the beta.\n- `allowedEmails` sent for a campaign with more than one sender identity.\n- Unrecognized field on a step or branch (e.g. `delay` is not a valid key on conditional `onAccepted`/`onTrue`/`onFalse` branches — use a dedicated wait/delay node inside the branch instead).\n- Required field missing (the response `errors[].path` indicates which body location failed validation).","content":{"_mintlify/placeholder":{"schema":{"type":"object","description":""}}}},"d19961b1-1a8e-44ed-8379-e180b5f8af38":{"description":"Unprocessable entity - the campaign steps are structurally invalid.\nCommon causes the agent should fix before retrying:\n- EMAIL_OPENED / NOT_EMAIL_OPENED conditions may only appear *after* an EMAIL node in the same branch. Add the EMAIL node first or remove the condition.\n- Every conditional branch (`onAccepted`/`onNotAccepted`/`onTrue`/`onFalse`) must contain at least one node. Use `[{ \"type\": \"end\" }]` to terminate a branch explicitly instead of leaving it empty.\n- Templated fields (e.g. `{firstName}`, `{companyName}`) must reference AI variables or standard fields that exist for the campaign's entity. Use GET /v1/contacts/fields or GET /v1/companies/fields to discover valid placeholders before composing message bodies.\n- Reply emails inside the same thread may omit `subject`, but the first email in any thread must have a non-empty subject.\n- An `email` step with `thread: \"new_thread\"` must have a non-empty `subject` unless it uses `aiMessageId`.\n- `aiMessageId` must reference a workspace AI message (GET /v1/ai-variables/ai-messages) or a prompt-library entry (GET /v1/prompt-library/ai-messages) whose channel matches the step type, and cannot be combined with manual `content`/`subject`.","content":{"_mintlify/placeholder":{"schema":{"type":"object","description":""}}}}},"security":{"662a52a8-3164-45b1-a27a-868a50479843":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","scopes":[],"schemeName":"apiKey"}}},"servers":{"0583bc01-7d89-47ba-9860-852cd05faae2":{"url":"https://openapi.enginy.ai","description":"Configured server"}},"processedSecurityOptions":[{"schemes":[{"scheme":{"type":"apiKey","in":"header","name":"x-api-key","scopes":[],"schemeName":"apiKey"},"schema":{"title":"x-api-key","type":"string","isRequired":true,"typeLabel":"string","deprecated":false},"schemeName":"apiKey","name":"x-api-key"}],"title":"apiKey"}]},"originalFileLocation":"https://openapi.enginy.ai/definitions.json"},"subdomain":"genesyai","siteName":"Enginy API Docs","title":"Validate campaign - Enginy API Docs","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-10-23T06:52:01.456Z","originalFileLocation":"api-reference/campaigns/validate-campaign.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before POST /v1/campaigns when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign. This endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy. Create a draft campaign from the public steps format. How to structure the request: - Start steps with the first action that should happen. - Keep adding the next action in the order it should run. - Use condition when you want to branch on lead data. - Use linkedin_connection with waitForAcceptance when you want to branch on connection acceptance after sending a connection request. Omit waitForAcceptance for a fire-and-forget connection request. - Use linkedin_message with content, attachment, or both. Attachment-only messages are valid. - Use whatsapp_message only when the workspace and sending identity are configured for WhatsApp. - Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches. Senders: - Pass identityIds (primary first) to send from several identities; the campaign spreads its contacts across them. identityId is the single-sender shorthand. - More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with details.code MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED. - Use GET /v1/identities to find identity IDs. allowedEmails only applies to single-sender campaigns. AI-generated messages (message library): - email, linkedin_message, linkedin_inmail, and linkedin_connection steps accept aiMessageId instead of manual content — the linked AI message writes a personalized message per contact at send time. - To reuse a prompt-library (\u0026quot;message library\u0026quot;) prompt, list entries with GET /v1/prompt-library/ai-messages and pass the entry id as the step aiMessageId. The step gets its own copy automatically. - To reuse or create a workspace AI message, use GET /v1/ai-variables/ai-messages or POST /v1/ai-variables/ai-messages (full control over prompt, toneId, model, outputLength, and splitMessages), then pass its id as aiMessageId. - The AI message channel must match the step type (EMAIL, LINKEDIN, LINKEDIN_INMAIL, or LINKEDIN_CONNECTION). - Never combine aiMessageId with manual content or subject on the same step — for email and linkedin_inmail the AI message generates the subject too. Email threading (Reply / New thread): - Set thread on an email step to pick how it is sent. reply sends it in the thread of the previous email, like the editor\u0026#x27;s Reply option, and needs no subject. new_thread starts a separate thread under the step\u0026#x27;s own subject. - When thread is omitted, a follow-up email without subject replies and one with subject is sent under that subject. Set thread explicitly whenever the choice matters, especially for aiMessageId follow-ups, which otherwise go out under their generated subject. Before you send a request: - Send an ordered steps array that describes the campaign flow from first action to last action. - Use branch arrays on condition and linkedin_connection steps to describe branch-specific work. - Place shared follow-up steps after a branching step when both live branches should continue together. - waitForAcceptance.unit must be days and waitForAcceptance.value must be an integer greater than or equal to 1. - Use GET /v1/tasks/owners first when your campaign includes task steps. If owners are returned, every task step must include ownerId. - Validation errors use full paths like steps[0].onTrue[1].subject so nested input issues are easy to fix. - Personalization placeholders use Enginy single-brace syntax such as {firstName}, {companyName}, and {identity.name}. Do not send Handlebars-style placeholders like {{firstName}}. - Placeholders must reference real fields — discover them with GET /v1/contacts/fields. Unknown placeholders are rejected with a 422. The company aliases {company}, {companyName}, and {companyDomain} are accepted and stored as the canonical company columns. - To modify an existing DRAFT campaign in place, prefer PATCH /v1/campaign/{campaignId} over creating a new one. To rewrite into a new campaign, start from the steps returned by GET /v1/campaign/{campaignId}. Either way the read-only stepId, connectionId, nextConnectionId, and advanced fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their aiMessageId binding. Supported condition.type values: - has_linkedin_profile: onTrue runs when the lead has a LinkedIn profile. - has_professional_email: onTrue runs when the lead has a professional email. - is_already_connected: onTrue runs when the lead is already a LinkedIn connection. - has_been_contacted: onTrue runs when the lead has already been contacted. - has_whatsapp_account: onTrue runs when the lead has a WhatsApp account. - email_opened: onTrue runs when the lead opens the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - email_clicked: onTrue runs when the lead clicks a link in the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - linkedin_message_viewed: onTrue runs when the lead views the preceding LinkedIn message within waitFor. Must follow a LinkedIn message step (linkedin_message, linkedin_message_bundle, linkedin_voice_message, or linkedin_inmail). Requires waitFor (value, unit). - task_completed: onTrue runs when the preceding task step is marked complete within waitFor. Must follow a task step. Requires waitFor (value, unit). - connection_accepted: onTrue runs when the lead accepts a previous LinkedIn connection request within waitFor. Requires waitFor (value, unit). - lead_field: onTrue runs when the contact field matches the operator (and value, where applicable). Use this to branch on any built-in column or custom field. Requires field and operator. value is required for every operator except IS_EMPTY and IS_NOT_EMPTY. Supported operators: EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS, IS_EMPTY, IS_NOT_EMPTY. Branch fields: - condition uses onTrue and onFalse. - linkedin_connection uses onAccepted and onNotAccepted only when waitForAcceptance is set. - Use [{ \u0026quot;type\u0026quot;: \u0026quot;end\u0026quot; }] when a branch should stop explicitly. Routing into another campaign: - Use add_to_another_campaign with a campaignId to route the lead into an existing campaign in the same workspace. - The referenced campaignId must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each campaignId to the equivalent campaign in the destination workspace before posting. - Can be followed by additional steps in the same branch. For examples and a full step-by-step guide, see /api-reference/campaigns-guide in the docs site. \u0026gt; Required scope: CAMPAIGNS_WRITE \u0026gt; \u0026gt; Rate limit: 30 requests per minute\">\u003cmeta data-mint-head name=\"application-name\" content=\"Enginy API Docs\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Validate campaign - Enginy API Docs\">\u003cmeta data-mint-head property=\"og:description\" content=\"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before POST /v1/campaigns when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign. This endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy. Create a draft campaign from the public steps format. How to structure the request: - Start steps with the first action that should happen. - Keep adding the next action in the order it should run. - Use condition when you want to branch on lead data. - Use linkedin_connection with waitForAcceptance when you want to branch on connection acceptance after sending a connection request. Omit waitForAcceptance for a fire-and-forget connection request. - Use linkedin_message with content, attachment, or both. Attachment-only messages are valid. - Use whatsapp_message only when the workspace and sending identity are configured for WhatsApp. - Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches. Senders: - Pass identityIds (primary first) to send from several identities; the campaign spreads its contacts across them. identityId is the single-sender shorthand. - More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with details.code MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED. - Use GET /v1/identities to find identity IDs. allowedEmails only applies to single-sender campaigns. AI-generated messages (message library): - email, linkedin_message, linkedin_inmail, and linkedin_connection steps accept aiMessageId instead of manual content — the linked AI message writes a personalized message per contact at send time. - To reuse a prompt-library (\u0026quot;message library\u0026quot;) prompt, list entries with GET /v1/prompt-library/ai-messages and pass the entry id as the step aiMessageId. The step gets its own copy automatically. - To reuse or create a workspace AI message, use GET /v1/ai-variables/ai-messages or POST /v1/ai-variables/ai-messages (full control over prompt, toneId, model, outputLength, and splitMessages), then pass its id as aiMessageId. - The AI message channel must match the step type (EMAIL, LINKEDIN, LINKEDIN_INMAIL, or LINKEDIN_CONNECTION). - Never combine aiMessageId with manual content or subject on the same step — for email and linkedin_inmail the AI message generates the subject too. Email threading (Reply / New thread): - Set thread on an email step to pick how it is sent. reply sends it in the thread of the previous email, like the editor\u0026#x27;s Reply option, and needs no subject. new_thread starts a separate thread under the step\u0026#x27;s own subject. - When thread is omitted, a follow-up email without subject replies and one with subject is sent under that subject. Set thread explicitly whenever the choice matters, especially for aiMessageId follow-ups, which otherwise go out under their generated subject. Before you send a request: - Send an ordered steps array that describes the campaign flow from first action to last action. - Use branch arrays on condition and linkedin_connection steps to describe branch-specific work. - Place shared follow-up steps after a branching step when both live branches should continue together. - waitForAcceptance.unit must be days and waitForAcceptance.value must be an integer greater than or equal to 1. - Use GET /v1/tasks/owners first when your campaign includes task steps. If owners are returned, every task step must include ownerId. - Validation errors use full paths like steps[0].onTrue[1].subject so nested input issues are easy to fix. - Personalization placeholders use Enginy single-brace syntax such as {firstName}, {companyName}, and {identity.name}. Do not send Handlebars-style placeholders like {{firstName}}. - Placeholders must reference real fields — discover them with GET /v1/contacts/fields. Unknown placeholders are rejected with a 422. The company aliases {company}, {companyName}, and {companyDomain} are accepted and stored as the canonical company columns. - To modify an existing DRAFT campaign in place, prefer PATCH /v1/campaign/{campaignId} over creating a new one. To rewrite into a new campaign, start from the steps returned by GET /v1/campaign/{campaignId}. Either way the read-only stepId, connectionId, nextConnectionId, and advanced fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their aiMessageId binding. Supported condition.type values: - has_linkedin_profile: onTrue runs when the lead has a LinkedIn profile. - has_professional_email: onTrue runs when the lead has a professional email. - is_already_connected: onTrue runs when the lead is already a LinkedIn connection. - has_been_contacted: onTrue runs when the lead has already been contacted. - has_whatsapp_account: onTrue runs when the lead has a WhatsApp account. - email_opened: onTrue runs when the lead opens the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - email_clicked: onTrue runs when the lead clicks a link in the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - linkedin_message_viewed: onTrue runs when the lead views the preceding LinkedIn message within waitFor. Must follow a LinkedIn message step (linkedin_message, linkedin_message_bundle, linkedin_voice_message, or linkedin_inmail). Requires waitFor (value, unit). - task_completed: onTrue runs when the preceding task step is marked complete within waitFor. Must follow a task step. Requires waitFor (value, unit). - connection_accepted: onTrue runs when the lead accepts a previous LinkedIn connection request within waitFor. Requires waitFor (value, unit). - lead_field: onTrue runs when the contact field matches the operator (and value, where applicable). Use this to branch on any built-in column or custom field. Requires field and operator. value is required for every operator except IS_EMPTY and IS_NOT_EMPTY. Supported operators: EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS, IS_EMPTY, IS_NOT_EMPTY. Branch fields: - condition uses onTrue and onFalse. - linkedin_connection uses onAccepted and onNotAccepted only when waitForAcceptance is set. - Use [{ \u0026quot;type\u0026quot;: \u0026quot;end\u0026quot; }] when a branch should stop explicitly. Routing into another campaign: - Use add_to_another_campaign with a campaignId to route the lead into an existing campaign in the same workspace. - The referenced campaignId must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each campaignId to the equivalent campaign in the destination workspace before posting. - Can be followed by additional steps in the same branch. For examples and a full step-by-step guide, see /api-reference/campaigns-guide in the docs site. \u0026gt; Required scope: CAMPAIGNS_WRITE \u0026gt; \u0026gt; Rate limit: 30 requests per minute\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Enginy API Docs\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://genesyai.mintlify.app/_mintlify/api/og/v2?division=Campaigns\u0026amp;title=Validate+campaign\u0026amp;description=Validate+a+simplified+public+campaign+payload+against+the+public+step+schema+without+creating+a+campaign.+Use+this+before+%60POST+%2Fv1%2Fcampaigns%60+when+you+already+\u0026amp;theme=f2b1053b8a2ac574395c5658\u0026amp;signature=8c4b8920110275622d6d5a34ea0430939c68a4e8d0a93a9c09ce5828b873c067\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Validate campaign - Enginy API Docs\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before POST /v1/campaigns when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign. This endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy. Create a draft campaign from the public steps format. How to structure the request: - Start steps with the first action that should happen. - Keep adding the next action in the order it should run. - Use condition when you want to branch on lead data. - Use linkedin_connection with waitForAcceptance when you want to branch on connection acceptance after sending a connection request. Omit waitForAcceptance for a fire-and-forget connection request. - Use linkedin_message with content, attachment, or both. Attachment-only messages are valid. - Use whatsapp_message only when the workspace and sending identity are configured for WhatsApp. - Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches. Senders: - Pass identityIds (primary first) to send from several identities; the campaign spreads its contacts across them. identityId is the single-sender shorthand. - More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with details.code MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED. - Use GET /v1/identities to find identity IDs. allowedEmails only applies to single-sender campaigns. AI-generated messages (message library): - email, linkedin_message, linkedin_inmail, and linkedin_connection steps accept aiMessageId instead of manual content — the linked AI message writes a personalized message per contact at send time. - To reuse a prompt-library (\u0026quot;message library\u0026quot;) prompt, list entries with GET /v1/prompt-library/ai-messages and pass the entry id as the step aiMessageId. The step gets its own copy automatically. - To reuse or create a workspace AI message, use GET /v1/ai-variables/ai-messages or POST /v1/ai-variables/ai-messages (full control over prompt, toneId, model, outputLength, and splitMessages), then pass its id as aiMessageId. - The AI message channel must match the step type (EMAIL, LINKEDIN, LINKEDIN_INMAIL, or LINKEDIN_CONNECTION). - Never combine aiMessageId with manual content or subject on the same step — for email and linkedin_inmail the AI message generates the subject too. Email threading (Reply / New thread): - Set thread on an email step to pick how it is sent. reply sends it in the thread of the previous email, like the editor\u0026#x27;s Reply option, and needs no subject. new_thread starts a separate thread under the step\u0026#x27;s own subject. - When thread is omitted, a follow-up email without subject replies and one with subject is sent under that subject. Set thread explicitly whenever the choice matters, especially for aiMessageId follow-ups, which otherwise go out under their generated subject. Before you send a request: - Send an ordered steps array that describes the campaign flow from first action to last action. - Use branch arrays on condition and linkedin_connection steps to describe branch-specific work. - Place shared follow-up steps after a branching step when both live branches should continue together. - waitForAcceptance.unit must be days and waitForAcceptance.value must be an integer greater than or equal to 1. - Use GET /v1/tasks/owners first when your campaign includes task steps. If owners are returned, every task step must include ownerId. - Validation errors use full paths like steps[0].onTrue[1].subject so nested input issues are easy to fix. - Personalization placeholders use Enginy single-brace syntax such as {firstName}, {companyName}, and {identity.name}. Do not send Handlebars-style placeholders like {{firstName}}. - Placeholders must reference real fields — discover them with GET /v1/contacts/fields. Unknown placeholders are rejected with a 422. The company aliases {company}, {companyName}, and {companyDomain} are accepted and stored as the canonical company columns. - To modify an existing DRAFT campaign in place, prefer PATCH /v1/campaign/{campaignId} over creating a new one. To rewrite into a new campaign, start from the steps returned by GET /v1/campaign/{campaignId}. Either way the read-only stepId, connectionId, nextConnectionId, and advanced fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their aiMessageId binding. Supported condition.type values: - has_linkedin_profile: onTrue runs when the lead has a LinkedIn profile. - has_professional_email: onTrue runs when the lead has a professional email. - is_already_connected: onTrue runs when the lead is already a LinkedIn connection. - has_been_contacted: onTrue runs when the lead has already been contacted. - has_whatsapp_account: onTrue runs when the lead has a WhatsApp account. - email_opened: onTrue runs when the lead opens the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - email_clicked: onTrue runs when the lead clicks a link in the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - linkedin_message_viewed: onTrue runs when the lead views the preceding LinkedIn message within waitFor. Must follow a LinkedIn message step (linkedin_message, linkedin_message_bundle, linkedin_voice_message, or linkedin_inmail). Requires waitFor (value, unit). - task_completed: onTrue runs when the preceding task step is marked complete within waitFor. Must follow a task step. Requires waitFor (value, unit). - connection_accepted: onTrue runs when the lead accepts a previous LinkedIn connection request within waitFor. Requires waitFor (value, unit). - lead_field: onTrue runs when the contact field matches the operator (and value, where applicable). Use this to branch on any built-in column or custom field. Requires field and operator. value is required for every operator except IS_EMPTY and IS_NOT_EMPTY. Supported operators: EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS, IS_EMPTY, IS_NOT_EMPTY. Branch fields: - condition uses onTrue and onFalse. - linkedin_connection uses onAccepted and onNotAccepted only when waitForAcceptance is set. - Use [{ \u0026quot;type\u0026quot;: \u0026quot;end\u0026quot; }] when a branch should stop explicitly. Routing into another campaign: - Use add_to_another_campaign with a campaignId to route the lead into an existing campaign in the same workspace. - The referenced campaignId must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each campaignId to the equivalent campaign in the destination workspace before posting. - Can be followed by additional steps in the same branch. For examples and a full step-by-step guide, see /api-reference/campaigns-guide in the docs site. \u0026gt; Required scope: CAMPAIGNS_WRITE \u0026gt; \u0026gt; Rate limit: 30 requests per minute\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://genesyai.mintlify.app/_mintlify/api/og/v2?division=Campaigns\u0026amp;title=Validate+campaign\u0026amp;description=Validate+a+simplified+public+campaign+payload+against+the+public+step+schema+without+creating+a+campaign.+Use+this+before+%60POST+%2Fv1%2Fcampaigns%60+when+you+already+\u0026amp;theme=f2b1053b8a2ac574395c5658\u0026amp;signature=8c4b8920110275622d6d5a34ea0430939c68a4e8d0a93a9c09ce5828b873c067\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/genesyai/KC8b0RpuY3wjR47N/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Enginy API Docs\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#1EC371\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/api-reference/campaigns/validate-campaign.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.enginy.ai/#organization\",\"name\":\"Enginy API Docs\",\"url\":\"https://docs.enginy.ai\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/genesyai/WDIQAoU7a8DJAxu4/logo/enginy-dark.svg?fit=max\u0026auto=format\u0026n=WDIQAoU7a8DJAxu4\u0026q=85\u0026s=939acf98e42633af0faae66d57713e73\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.enginy.ai#website\",\"name\":\"Enginy API Docs\",\"url\":\"https://docs.enginy.ai\",\"publisher\":{\"@id\":\"https://docs.enginy.ai/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign#webpage\",\"url\":\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign\",\"name\":\"Validate campaign\",\"description\":\"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before POST /v1/campaigns when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign. This endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy. Create a draft campaign from the public steps format. How to structure the request: - Start steps with the first action that should happen. - Keep adding the next action in the order it should run. - Use condition when you want to branch on lead data. - Use linkedin_connection with waitForAcceptance when you want to branch on connection acceptance after sending a connection request. Omit waitForAcceptance for a fire-and-forget connection request. - Use linkedin_message with content, attachment, or both. Attachment-only messages are valid. - Use whatsapp_message only when the workspace and sending identity are configured for WhatsApp. - Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches. Senders: - Pass identityIds (primary first) to send from several identities; the campaign spreads its contacts across them. identityId is the single-sender shorthand. - More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with details.code MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED. - Use GET /v1/identities to find identity IDs. allowedEmails only applies to single-sender campaigns. AI-generated messages (message library): - email, linkedin_message, linkedin_inmail, and linkedin_connection steps accept aiMessageId instead of manual content — the linked AI message writes a personalized message per contact at send time. - To reuse a prompt-library (\\\"message library\\\") prompt, list entries with GET /v1/prompt-library/ai-messages and pass the entry id as the step aiMessageId. The step gets its own copy automatically. - To reuse or create a workspace AI message, use GET /v1/ai-variables/ai-messages or POST /v1/ai-variables/ai-messages (full control over prompt, toneId, model, outputLength, and splitMessages), then pass its id as aiMessageId. - The AI message channel must match the step type (EMAIL, LINKEDIN, LINKEDIN_INMAIL, or LINKEDIN_CONNECTION). - Never combine aiMessageId with manual content or subject on the same step — for email and linkedin_inmail the AI message generates the subject too. Email threading (Reply / New thread): - Set thread on an email step to pick how it is sent. reply sends it in the thread of the previous email, like the editor's Reply option, and needs no subject. new_thread starts a separate thread under the step's own subject. - When thread is omitted, a follow-up email without subject replies and one with subject is sent under that subject. Set thread explicitly whenever the choice matters, especially for aiMessageId follow-ups, which otherwise go out under their generated subject. Before you send a request: - Send an ordered steps array that describes the campaign flow from first action to last action. - Use branch arrays on condition and linkedin_connection steps to describe branch-specific work. - Place shared follow-up steps after a branching step when both live branches should continue together. - waitForAcceptance.unit must be days and waitForAcceptance.value must be an integer greater than or equal to 1. - Use GET /v1/tasks/owners first when your campaign includes task steps. If owners are returned, every task step must include ownerId. - Validation errors use full paths like steps[0].onTrue[1].subject so nested input issues are easy to fix. - Personalization placeholders use Enginy single-brace syntax such as {firstName}, {companyName}, and {identity.name}. Do not send Handlebars-style placeholders like {{firstName}}. - Placeholders must reference real fields — discover them with GET /v1/contacts/fields. Unknown placeholders are rejected with a 422. The company aliases {company}, {companyName}, and {companyDomain} are accepted and stored as the canonical company columns. - To modify an existing DRAFT campaign in place, prefer PATCH /v1/campaign/{campaignId} over creating a new one. To rewrite into a new campaign, start from the steps returned by GET /v1/campaign/{campaignId}. Either way the read-only stepId, connectionId, nextConnectionId, and advanced fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their aiMessageId binding. Supported condition.type values: - has_linkedin_profile: onTrue runs when the lead has a LinkedIn profile. - has_professional_email: onTrue runs when the lead has a professional email. - is_already_connected: onTrue runs when the lead is already a LinkedIn connection. - has_been_contacted: onTrue runs when the lead has already been contacted. - has_whatsapp_account: onTrue runs when the lead has a WhatsApp account. - email_opened: onTrue runs when the lead opens the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - email_clicked: onTrue runs when the lead clicks a link in the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - linkedin_message_viewed: onTrue runs when the lead views the preceding LinkedIn message within waitFor. Must follow a LinkedIn message step (linkedin_message, linkedin_message_bundle, linkedin_voice_message, or linkedin_inmail). Requires waitFor (value, unit). - task_completed: onTrue runs when the preceding task step is marked complete within waitFor. Must follow a task step. Requires waitFor (value, unit). - connection_accepted: onTrue runs when the lead accepts a previous LinkedIn connection request within waitFor. Requires waitFor (value, unit). - lead_field: onTrue runs when the contact field matches the operator (and value, where applicable). Use this to branch on any built-in column or custom field. Requires field and operator. value is required for every operator except IS_EMPTY and IS_NOT_EMPTY. Supported operators: EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS, IS_EMPTY, IS_NOT_EMPTY. Branch fields: - condition uses onTrue and onFalse. - linkedin_connection uses onAccepted and onNotAccepted only when waitForAcceptance is set. - Use [{ \\\"type\\\": \\\"end\\\" }] when a branch should stop explicitly. Routing into another campaign: - Use add_to_another_campaign with a campaignId to route the lead into an existing campaign in the same workspace. - The referenced campaignId must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each campaignId to the equivalent campaign in the destination workspace before posting. - Can be followed by additional steps in the same branch. For examples and a full step-by-step guide, see /api-reference/campaigns-guide in the docs site. > Required scope: CAMPAIGNS_WRITE > > Rate limit: 30 requests per minute\",\"dateModified\":\"2026-10-23T06:52:01.456Z\",\"isPartOf\":{\"@id\":\"https://docs.enginy.ai#website\"},\"breadcrumb\":{\"@id\":\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Endpoints\",\"item\":\"https://docs.enginy.ai/api-reference/ai-variables/list-contact-ai-variable-folders\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Campaigns\",\"item\":\"https://docs.enginy.ai/api-reference/campaigns/list-campaign-folders\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Validate campaign\",\"item\":\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign\"}]},{\"@type\":[\"TechArticle\",\"APIReference\"],\"@id\":\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign#apireference\",\"headline\":\"Validate campaign\",\"name\":\"Validate campaign\",\"description\":\"Validate a simplified public campaign payload against the public step schema without creating a campaign. Use this before POST /v1/campaigns when you already have a proposed request body. This validates only the submitted payload and does not read or inspect an existing saved campaign. This endpoint validates only the submitted request body. It does not load, inspect, or verify an already-saved campaign in Enginy. Create a draft campaign from the public steps format. How to structure the request: - Start steps with the first action that should happen. - Keep adding the next action in the order it should run. - Use condition when you want to branch on lead data. - Use linkedin_connection with waitForAcceptance when you want to branch on connection acceptance after sending a connection request. Omit waitForAcceptance for a fire-and-forget connection request. - Use linkedin_message with content, attachment, or both. Attachment-only messages are valid. - Use whatsapp_message only when the workspace and sending identity are configured for WhatsApp. - Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches. Senders: - Pass identityIds (primary first) to send from several identities; the campaign spreads its contacts across them. identityId is the single-sender shorthand. - More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with details.code MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED. - Use GET /v1/identities to find identity IDs. allowedEmails only applies to single-sender campaigns. AI-generated messages (message library): - email, linkedin_message, linkedin_inmail, and linkedin_connection steps accept aiMessageId instead of manual content — the linked AI message writes a personalized message per contact at send time. - To reuse a prompt-library (\\\"message library\\\") prompt, list entries with GET /v1/prompt-library/ai-messages and pass the entry id as the step aiMessageId. The step gets its own copy automatically. - To reuse or create a workspace AI message, use GET /v1/ai-variables/ai-messages or POST /v1/ai-variables/ai-messages (full control over prompt, toneId, model, outputLength, and splitMessages), then pass its id as aiMessageId. - The AI message channel must match the step type (EMAIL, LINKEDIN, LINKEDIN_INMAIL, or LINKEDIN_CONNECTION). - Never combine aiMessageId with manual content or subject on the same step — for email and linkedin_inmail the AI message generates the subject too. Email threading (Reply / New thread): - Set thread on an email step to pick how it is sent. reply sends it in the thread of the previous email, like the editor's Reply option, and needs no subject. new_thread starts a separate thread under the step's own subject. - When thread is omitted, a follow-up email without subject replies and one with subject is sent under that subject. Set thread explicitly whenever the choice matters, especially for aiMessageId follow-ups, which otherwise go out under their generated subject. Before you send a request: - Send an ordered steps array that describes the campaign flow from first action to last action. - Use branch arrays on condition and linkedin_connection steps to describe branch-specific work. - Place shared follow-up steps after a branching step when both live branches should continue together. - waitForAcceptance.unit must be days and waitForAcceptance.value must be an integer greater than or equal to 1. - Use GET /v1/tasks/owners first when your campaign includes task steps. If owners are returned, every task step must include ownerId. - Validation errors use full paths like steps[0].onTrue[1].subject so nested input issues are easy to fix. - Personalization placeholders use Enginy single-brace syntax such as {firstName}, {companyName}, and {identity.name}. Do not send Handlebars-style placeholders like {{firstName}}. - Placeholders must reference real fields — discover them with GET /v1/contacts/fields. Unknown placeholders are rejected with a 422. The company aliases {company}, {companyName}, and {companyDomain} are accepted and stored as the canonical company columns. - To modify an existing DRAFT campaign in place, prefer PATCH /v1/campaign/{campaignId} over creating a new one. To rewrite into a new campaign, start from the steps returned by GET /v1/campaign/{campaignId}. Either way the read-only stepId, connectionId, nextConnectionId, and advanced fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their aiMessageId binding. Supported condition.type values: - has_linkedin_profile: onTrue runs when the lead has a LinkedIn profile. - has_professional_email: onTrue runs when the lead has a professional email. - is_already_connected: onTrue runs when the lead is already a LinkedIn connection. - has_been_contacted: onTrue runs when the lead has already been contacted. - has_whatsapp_account: onTrue runs when the lead has a WhatsApp account. - email_opened: onTrue runs when the lead opens the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - email_clicked: onTrue runs when the lead clicks a link in the preceding email within waitFor. Must follow an email step. Requires waitFor (value, unit). - linkedin_message_viewed: onTrue runs when the lead views the preceding LinkedIn message within waitFor. Must follow a LinkedIn message step (linkedin_message, linkedin_message_bundle, linkedin_voice_message, or linkedin_inmail). Requires waitFor (value, unit). - task_completed: onTrue runs when the preceding task step is marked complete within waitFor. Must follow a task step. Requires waitFor (value, unit). - connection_accepted: onTrue runs when the lead accepts a previous LinkedIn connection request within waitFor. Requires waitFor (value, unit). - lead_field: onTrue runs when the contact field matches the operator (and value, where applicable). Use this to branch on any built-in column or custom field. Requires field and operator. value is required for every operator except IS_EMPTY and IS_NOT_EMPTY. Supported operators: EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS, IS_EMPTY, IS_NOT_EMPTY. Branch fields: - condition uses onTrue and onFalse. - linkedin_connection uses onAccepted and onNotAccepted only when waitForAcceptance is set. - Use [{ \\\"type\\\": \\\"end\\\" }] when a branch should stop explicitly. Routing into another campaign: - Use add_to_another_campaign with a campaignId to route the lead into an existing campaign in the same workspace. - The referenced campaignId must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each campaignId to the equivalent campaign in the destination workspace before posting. - Can be followed by additional steps in the same branch. For examples and a full step-by-step guide, see /api-reference/campaigns-guide in the docs site. > Required scope: CAMPAIGNS_WRITE > > Rate limit: 30 requests per minute\",\"url\":\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign\",\"mainEntityOfPage\":{\"@id\":\"https://docs.enginy.ai/api-reference/campaigns/validate-campaign#webpage\"},\"dateModified\":\"2026-10-23T06:52:01.456Z\",\"publisher\":{\"@id\":\"https://docs.enginy.ai/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.enginy.ai#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"Enginy API Docs\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"Enginy API Docs","nav":{"global":{"anchors":[{"anchor":"Help Center","icon":"book-open-cover","href":"https://help.enginy.ai/"},{"anchor":"API Dashboard","icon":"key","href":"https://app.enginy.ai/api-access"}]},"tabs":[{"tab":"Guides","pages":[{"title":"Guides","href":""}]},{"tab":"API reference","groups":[{"group":"API documentation","pages":[{"title":"Introduction","icon":"compass","href":"/api-reference/introduction"},{"title":"Searching Contacts And Companies","icon":"magnifying-glass","href":"/api-reference/searching-contacts-companies"},{"title":"Contact and Company Tags","icon":"tags","href":"/api-reference/contact-company-tags-guide"},{"title":"Campaign Creation Guide","icon":"paper-plane","href":"/api-reference/campaigns-guide"},{"title":"Identities Guide","icon":"id-badge","href":"/api-reference/identities-guide"},{"title":"Rate Limits","icon":"gauge-high","href":"/api-reference/rate-limits"},{"title":"Actions guide","icon":"bolt","href":"/api-reference/actions-guide"},{"title":"AI Variables","icon":"wand-magic-sparkles","href":"/api-reference/ai-variables-guide"},{"title":"AI Playbook","icon":"book-open","href":"/api-reference/ai-playbook-guide"},{"title":"Webhooks guide","icon":"webhook","href":"/api-reference/webhooks-guide"},{"title":"Workflows API Overview","icon":"diagram-project","href":"/api-reference/workflows-guide"},{"title":"Workflows API Cookbook","icon":"book-open","href":"/api-reference/workflows-cookbook"},{"title":"Social Selling","href":"/api-reference/social-selling-guide"}]},{"group":"Endpoints","pages":[{"group":"AI Variables","pages":[{"title":"List contact AI variable folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/contacts","href":"/api-reference/ai-variables/list-contact-ai-variable-folders"},{"title":"Create contact AI variable folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables/folders/contacts","href":"/api-reference/ai-variables/create-contact-ai-variable-folder"},{"title":"Get contact AI variable folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/contacts/{folderId}","href":"/api-reference/ai-variables/get-contact-ai-variable-folder"},{"title":"Delete contact AI variable folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/folders/contacts/{folderId}","href":"/api-reference/ai-variables/delete-contact-ai-variable-folder"},{"title":"Update contact AI variable folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/folders/contacts/{folderId}","href":"/api-reference/ai-variables/update-contact-ai-variable-folder"},{"title":"List company AI variable folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/companies","href":"/api-reference/ai-variables/list-company-ai-variable-folders"},{"title":"Create company AI variable folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables/folders/companies","href":"/api-reference/ai-variables/create-company-ai-variable-folder"},{"title":"Get company AI variable folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/companies/{folderId}","href":"/api-reference/ai-variables/get-company-ai-variable-folder"},{"title":"Delete company AI variable folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/folders/companies/{folderId}","href":"/api-reference/ai-variables/delete-company-ai-variable-folder"},{"title":"Update company AI variable folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/folders/companies/{folderId}","href":"/api-reference/ai-variables/update-company-ai-variable-folder"},{"title":"List AI message folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/ai-messages","href":"/api-reference/ai-variables/list-ai-message-folders"},{"title":"Create AI message folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables/folders/ai-messages","href":"/api-reference/ai-variables/create-ai-message-folder"},{"title":"Get AI message folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/ai-messages/{folderId}","href":"/api-reference/ai-variables/get-ai-message-folder"},{"title":"Delete AI message folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/folders/ai-messages/{folderId}","href":"/api-reference/ai-variables/delete-ai-message-folder"},{"title":"Update AI message folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/folders/ai-messages/{folderId}","href":"/api-reference/ai-variables/update-ai-message-folder"},{"title":"List AI snippet folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/ai-snippets","href":"/api-reference/ai-variables/list-ai-snippet-folders"},{"title":"Create AI snippet folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables/folders/ai-snippets","href":"/api-reference/ai-variables/create-ai-snippet-folder"},{"title":"Get AI snippet folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/ai-snippets/{folderId}","href":"/api-reference/ai-variables/get-ai-snippet-folder"},{"title":"Delete AI snippet folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/folders/ai-snippets/{folderId}","href":"/api-reference/ai-variables/delete-ai-snippet-folder"},{"title":"Update AI snippet folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/folders/ai-snippets/{folderId}","href":"/api-reference/ai-variables/update-ai-snippet-folder"},{"title":"List message template folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/message-templates","href":"/api-reference/ai-variables/list-message-template-folders"},{"title":"Create message template folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables/folders/message-templates","href":"/api-reference/ai-variables/create-message-template-folder"},{"title":"Get message template folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/folders/message-templates/{folderId}","href":"/api-reference/ai-variables/get-message-template-folder"},{"title":"Delete message template folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/folders/message-templates/{folderId}","href":"/api-reference/ai-variables/delete-message-template-folder"},{"title":"Update message template folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/folders/message-templates/{folderId}","href":"/api-reference/ai-variables/update-message-template-folder"},{"title":"List AI messages","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/ai-messages","href":"/api-reference/ai-variables/list-ai-messages"},{"title":"Create an ai message","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables/ai-messages","href":"/api-reference/ai-variables/create-an-ai-message"},{"title":"List AI snippets","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/ai-snippets","href":"/api-reference/ai-variables/list-ai-snippets"},{"title":"Create an ai snippet","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables/ai-snippets","href":"/api-reference/ai-variables/create-an-ai-snippet"},{"title":"List message templates","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/message-templates","href":"/api-reference/ai-variables/list-message-templates"},{"title":"Create a message template","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables/message-templates","href":"/api-reference/ai-variables/create-a-message-template"},{"title":"Get an ai message","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/ai-messages/{aiMessageId}","href":"/api-reference/ai-variables/get-an-ai-message"},{"title":"Delete an ai message","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/ai-messages/{aiMessageId}","href":"/api-reference/ai-variables/delete-an-ai-message"},{"title":"Update an ai message","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/ai-messages/{aiMessageId}","href":"/api-reference/ai-variables/update-an-ai-message"},{"title":"Get an ai snippet","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/ai-snippets/{aiSnippetId}","href":"/api-reference/ai-variables/get-an-ai-snippet"},{"title":"Delete an ai snippet","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/ai-snippets/{aiSnippetId}","href":"/api-reference/ai-variables/delete-an-ai-snippet"},{"title":"Update an ai snippet","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/ai-snippets/{aiSnippetId}","href":"/api-reference/ai-variables/update-an-ai-snippet"},{"title":"Get a message template","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/message-templates/{messageTemplateId}","href":"/api-reference/ai-variables/get-a-message-template"},{"title":"Delete a message template","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/message-templates/{messageTemplateId}","href":"/api-reference/ai-variables/delete-a-message-template"},{"title":"Update a message template","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/message-templates/{messageTemplateId}","href":"/api-reference/ai-variables/update-a-message-template"},{"title":"List AI message tones","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/ai-message-tones","href":"/api-reference/ai-variables/list-ai-message-tones"},{"title":"List AI variables","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables","href":"/api-reference/ai-variables/list-ai-variables"},{"title":"Create an AI variable","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-variables","href":"/api-reference/ai-variables/create-an-ai-variable"},{"title":"Get an AI variable","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-variables/{aiVariableId}","href":"/api-reference/ai-variables/get-an-ai-variable"},{"title":"Delete an AI variable","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/ai-variables/{aiVariableId}","href":"/api-reference/ai-variables/delete-an-ai-variable"},{"title":"Update an AI variable","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/ai-variables/{aiVariableId}","href":"/api-reference/ai-variables/update-an-ai-variable"}]},{"group":"Contacts","pages":[{"title":"Get contact field metadata","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/contacts/fields","href":"/api-reference/contacts/get-contact-field-metadata"},{"title":"Create a contact custom field","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/contacts/fields","href":"/api-reference/contacts/create-a-contact-custom-field"},{"title":"Get contacts","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/contacts","href":"/api-reference/contacts/get-contacts"},{"title":"Search contacts with advanced filters","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/contacts/search","href":"/api-reference/contacts/search-contacts-with-advanced-filters"},{"title":"Get a single contact","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/contact/{contactId}","href":"/api-reference/contacts/get-a-single-contact"},{"title":"Delete a contact","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/contact/{contactId}","href":"/api-reference/contacts/delete-a-contact"},{"title":"Update contact fields","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/contact/{contactId}","href":"/api-reference/contacts/update-contact-fields"},{"title":"Create a new contact","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/contact","href":"/api-reference/contacts/create-a-new-contact"},{"title":"Bulk create contacts","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/contacts/bulk","href":"/api-reference/contacts/bulk-create-contacts"},{"title":"Get contact tags","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/contacts/tags","href":"/api-reference/contacts/get-contact-tags"},{"title":"Create a contact tag","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/contacts/tags","href":"/api-reference/contacts/create-a-contact-tag"},{"title":"Delete a contact tag","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/contacts/tag/{tagId}","href":"/api-reference/contacts/delete-a-contact-tag"},{"title":"Update a contact tag","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/contacts/tag/{tagId}","href":"/api-reference/contacts/update-a-contact-tag"},{"title":"Add tags to contacts","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/contacts/tags/add","href":"/api-reference/contacts/add-tags-to-contacts"},{"title":"Remove tags from contacts","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/contacts/tags/remove","href":"/api-reference/contacts/remove-tags-from-contacts"},{"title":"Find duplicate contacts","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/contacts/duplicates","href":"/api-reference/contacts/find-duplicate-contacts"}]},{"group":"Companies","pages":[{"title":"Get company field metadata","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/companies/fields","href":"/api-reference/companies/get-company-field-metadata"},{"title":"Create a new company","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/company","href":"/api-reference/companies/create-a-new-company"},{"title":"Bulk create companies","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/companies/bulk","href":"/api-reference/companies/bulk-create-companies"},{"title":"Get companies","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/companies","href":"/api-reference/companies/get-companies"},{"title":"Search companies with advanced filters","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/companies/search","href":"/api-reference/companies/search-companies-with-advanced-filters"},{"title":"Get a single company","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/company/{companyId}","href":"/api-reference/companies/get-a-single-company"},{"title":"Delete a company","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/company/{companyId}","href":"/api-reference/companies/delete-a-company"},{"title":"Update company fields","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/company/{companyId}","href":"/api-reference/companies/update-company-fields"},{"title":"Get company tags","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/companies/tags","href":"/api-reference/companies/get-company-tags"},{"title":"Create a company tag","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/companies/tags","href":"/api-reference/companies/create-a-company-tag"},{"title":"Delete a company tag","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/companies/tag/{tagId}","href":"/api-reference/companies/delete-a-company-tag"},{"title":"Update a company tag","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/companies/tag/{tagId}","href":"/api-reference/companies/update-a-company-tag"},{"title":"Add tags to companies","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/companies/tags/add","href":"/api-reference/companies/add-tags-to-companies"},{"title":"Remove tags from companies","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/companies/tags/remove","href":"/api-reference/companies/remove-tags-from-companies"},{"title":"Find duplicate companies","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/companies/duplicates","href":"/api-reference/companies/find-duplicate-companies"}]},{"group":"AI Finder","pages":[{"title":"Preview an AI Finder search","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-finder/previews","href":"/api-reference/ai-finder/preview-an-ai-finder-search"},{"title":"Refine an AI Finder preview","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-finder/previews/{previewId}/iterate","href":"/api-reference/ai-finder/refine-an-ai-finder-preview"},{"title":"Import an AI Finder preview","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-finder/previews/{previewId}/import","href":"/api-reference/ai-finder/import-an-ai-finder-preview"},{"title":"Fetch results from an AI Finder preview","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-finder/previews/{previewId}/results","href":"/api-reference/ai-finder/fetch-results-from-an-ai-finder-preview"}]},{"group":"AI Playbook","pages":[{"title":"Get AI playbook","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/ai-playbook","href":"/api-reference/ai-playbook/get-ai-playbook"},{"title":"Update AI playbook","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json put /v1/ai-playbook","href":"/api-reference/ai-playbook/update-ai-playbook"}]},{"group":"Identities","pages":[{"title":"Get identities","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/identities","href":"/api-reference/identities/get-identities"},{"title":"Get identity performance metrics","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/identities/performance","href":"/api-reference/identities/get-identity-performance-metrics"},{"title":"Update identity","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/identities/{identityId}","href":"/api-reference/identities/update-identity"},{"title":"Get identity mailboxes","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/identities/{identityId}/mailboxes","href":"/api-reference/identities/get-identity-mailboxes"},{"title":"Create or update an identity mailbox","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/identities/{identityId}/mailboxes","href":"/api-reference/identities/create-or-update-an-identity-mailbox"}]},{"group":"Campaigns","pages":[{"title":"List campaign folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/campaigns/folders","href":"/api-reference/campaigns/list-campaign-folders"},{"title":"Create campaign folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/campaigns/folders","href":"/api-reference/campaigns/create-campaign-folder"},{"title":"Get campaign folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/campaigns/folders/{folderId}","href":"/api-reference/campaigns/get-campaign-folder"},{"title":"Delete campaign folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/campaigns/folders/{folderId}","href":"/api-reference/campaigns/delete-campaign-folder"},{"title":"Update campaign folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/campaigns/folders/{folderId}","href":"/api-reference/campaigns/update-campaign-folder"},{"title":"Get campaigns","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/campaigns","href":"/api-reference/campaigns/get-campaigns"},{"title":"Create campaign","description":"Create a draft campaign from the public simplified sequence model. Best for new campaigns or rewrites that fit the supported public step types; this endpoint does not preserve arbitrary internal editor-only graph wiring from an existing campaign.\n\nResponses include direct Enginy app URLs when available. Responses include an `appUrl` that opens the created campaign in Enginy. MCP agents should return those URLs to users whenever they are present in the response.\n\nCreate a draft campaign from the public `steps` format.\n\nHow to structure the request:\n- Start `steps` with the first action that should happen.\n- Keep adding the next action in the order it should run.\n- Use `condition` when you want to branch on lead data.\n- Use `linkedin_connection` with `waitForAcceptance` when you want to branch on connection acceptance after sending a connection request. Omit `waitForAcceptance` for a fire-and-forget connection request.\n- Use `linkedin_message` with `content`, `attachment`, or both. Attachment-only messages are valid.\n- Use `whatsapp_message` only when the workspace and sending identity are configured for WhatsApp.\n- Put shared follow-up work after the branching step instead of duplicating the same steps inside both branches.\n\nSenders:\n- Pass `identityIds` (primary first) to send from several identities; the campaign spreads its contacts across them. `identityId` is the single-sender shorthand.\n- More than one sender requires the multi-identity campaigns beta on the workspace; without it the request is rejected with `details.code` `MULTI_IDENTITY_CAMPAIGNS_NOT_ENABLED`.\n- Use `GET /v1/identities` to find identity IDs. `allowedEmails` only applies to single-sender campaigns.\n\nAI-generated messages (message library):\n- `email`, `linkedin_message`, `linkedin_inmail`, and `linkedin_connection` steps accept `aiMessageId` instead of manual `content` — the linked AI message writes a personalized message per contact at send time.\n- To reuse a prompt-library (\"message library\") prompt, list entries with `GET /v1/prompt-library/ai-messages` and pass the entry `id` as the step `aiMessageId`. The step gets its own copy automatically.\n- To reuse or create a workspace AI message, use `GET /v1/ai-variables/ai-messages` or `POST /v1/ai-variables/ai-messages` (full control over `prompt`, `toneId`, `model`, `outputLength`, and `splitMessages`), then pass its `id` as `aiMessageId`.\n- The AI message `channel` must match the step type (`EMAIL`, `LINKEDIN`, `LINKEDIN_INMAIL`, or `LINKEDIN_CONNECTION`).\n- Never combine `aiMessageId` with manual `content` or `subject` on the same step — for `email` and `linkedin_inmail` the AI message generates the subject too.\n\nEmail threading (Reply / New thread):\n- Set `thread` on an `email` step to pick how it is sent. `reply` sends it in the thread of the previous email, like the editor's Reply option, and needs no `subject`. `new_thread` starts a separate thread under the step's own `subject`.\n- When `thread` is omitted, a follow-up email without `subject` replies and one with `subject` is sent under that subject. Set `thread` explicitly whenever the choice matters, especially for `aiMessageId` follow-ups, which otherwise go out under their generated subject.\n\nBefore you send a request:\n- Send an ordered `steps` array that describes the campaign flow from first action to last action.\n- Use branch arrays on `condition` and `linkedin_connection` steps to describe branch-specific work.\n- Place shared follow-up steps after a branching step when both live branches should continue together.\n- `waitForAcceptance.unit` must be `days` and `waitForAcceptance.value` must be an integer greater than or equal to `1`.\n- Use `GET /v1/tasks/owners` first when your campaign includes `task` steps. If owners are returned, every `task` step must include `ownerId`.\n- Validation errors use full paths like `steps[0].onTrue[1].subject` so nested input issues are easy to fix.\n- Personalization placeholders use Enginy single-brace syntax such as `{firstName}`, `{companyName}`, and `{identity.name}`. Do not send Handlebars-style placeholders like `{{firstName}}`.\n- Placeholders must reference real fields — discover them with `GET /v1/contacts/fields`. Unknown placeholders are rejected with a 422. The company aliases `{company}`, `{companyName}`, and `{companyDomain}` are accepted and stored as the canonical company columns.\n- To modify an existing DRAFT campaign in place, prefer `PATCH /v1/campaign/{campaignId}` over creating a new one. To rewrite into a new campaign, start from the `steps` returned by `GET /v1/campaign/{campaignId}`. Either way the read-only `stepId`, `connectionId`, `nextConnectionId`, and `advanced` fields are accepted and ignored. GET resolves saved manual templates and returns HTML email bodies. Steps PATCH rejects template emails whose original or replacement bodies contain dynamic references with 422 before replacement. Settings-only PATCH remains available. AI-generated steps retain their `aiMessageId` binding.\n\nSupported `condition.type` values:\n- `has_linkedin_profile`: `onTrue` runs when the lead has a LinkedIn profile.\n- `has_professional_email`: `onTrue` runs when the lead has a professional email.\n- `is_already_connected`: `onTrue` runs when the lead is already a LinkedIn connection.\n- `has_been_contacted`: `onTrue` runs when the lead has already been contacted.\n- `has_whatsapp_account`: `onTrue` runs when the lead has a WhatsApp account.\n- `email_opened`: `onTrue` runs when the lead opens the preceding email within `waitFor`. Must follow an `email` step. Requires `waitFor` (`value`, `unit`).\n- `email_clicked`: `onTrue` runs when the lead clicks a link in the preceding email within `waitFor`. Must follow an `email` step. Requires `waitFor` (`value`, `unit`).\n- `linkedin_message_viewed`: `onTrue` runs when the lead views the preceding LinkedIn message within `waitFor`. Must follow a LinkedIn message step (`linkedin_message`, `linkedin_message_bundle`, `linkedin_voice_message`, or `linkedin_inmail`). Requires `waitFor` (`value`, `unit`).\n- `task_completed`: `onTrue` runs when the preceding `task` step is marked complete within `waitFor`. Must follow a `task` step. Requires `waitFor` (`value`, `unit`).\n- `connection_accepted`: `onTrue` runs when the lead accepts a previous LinkedIn connection request within `waitFor`. Requires `waitFor` (`value`, `unit`).\n- `lead_field`: `onTrue` runs when the contact `field` matches the `operator` (and `value`, where applicable). Use this to branch on any built-in column or custom field. Requires `field` and `operator`. `value` is required for every operator except `IS_EMPTY` and `IS_NOT_EMPTY`. Supported operators: `EQUALS`, `NOT_EQUALS`, `CONTAINS`, `NOT_CONTAINS`, `GREATER_THAN`, `LESS_THAN`, `GREATER_THAN_OR_EQUALS`, `LESS_THAN_OR_EQUALS`, `IS_EMPTY`, `IS_NOT_EMPTY`.\n\nBranch fields:\n- `condition` uses `onTrue` and `onFalse`.\n- `linkedin_connection` uses `onAccepted` and `onNotAccepted` only when `waitForAcceptance` is set.\n- Use `[{ \"type\": \"end\" }]` when a branch should stop explicitly.\n\nRouting into another campaign:\n- Use `add_to_another_campaign` with a `campaignId` to route the lead into an existing campaign in the same workspace.\n- The referenced `campaignId` must belong to the same client as the API key. If you are cloning campaigns across workspaces, remap each `campaignId` to the equivalent campaign in the destination workspace before posting.\n- Can be followed by additional steps in the same branch.\n\nFor examples and a full step-by-step guide, see `/api-reference/campaigns-guide` in the docs site.\n\n > **Required scope:** `CAMPAIGNS_WRITE`\n >\n > **Rate limit:** 30 requests per minute","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/campaigns","href":"/api-reference/campaigns/create-campaign"},{"title":"Validate campaign","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/campaigns/validate","href":"/api-reference/campaigns/validate-campaign"},{"title":"Get a single campaign","description":"Retrieve a specific campaign as the public simplified sequence plus campaign settings. Use this when you need a readable campaign in the supported public step model, not the full internal editor graph.\n\nResponses include direct Enginy app URLs when available. Responses include an `appUrl` that opens the campaign overview in Enginy. MCP agents should return those URLs to users whenever they are present in the response.\n\n > **Required scope:** `CAMPAIGNS_READ`\n >\n > **Rate limit:** 100 requests per minute","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/campaign/{campaignId}","href":"/api-reference/campaigns/get-a-single-campaign"},{"title":"Update a draft campaign","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/campaign/{campaignId}","href":"/api-reference/campaigns/update-a-draft-campaign"},{"title":"Validate campaign draft","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/campaign/{campaignId}/validate-draft","href":"/api-reference/campaigns/validate-campaign-draft"},{"title":"Get campaign analytics","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/campaign/{campaignId}/analytics","href":"/api-reference/campaigns/get-campaign-analytics"},{"title":"Clone a campaign","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/campaign/{campaignId}/clone","href":"/api-reference/campaigns/clone-a-campaign"},{"title":"Update campaign status","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/campaign/{campaignId}/status","href":"/api-reference/campaigns/update-campaign-status"},{"title":"Add a contact to a campaign","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/add-contact-to-campaign","href":"/api-reference/campaigns/add-a-contact-to-a-campaign"},{"title":"Add a contact group to a campaign","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/add-contact-group-to-campaign","href":"/api-reference/campaigns/add-a-contact-group-to-a-campaign"},{"title":"Pause a contact in a campaign","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/pause-contact-in-campaign","href":"/api-reference/campaigns/pause-a-contact-in-a-campaign"},{"title":"Get conversations analytics","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/analytics/conversations","href":"/api-reference/campaigns/get-conversations-analytics"},{"title":"Get conversation messages","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/conversation/:conversationId/messages","href":"/api-reference/campaigns/get-conversation-messages"}]},{"group":"Tasks","pages":[{"title":"Get task owners","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/tasks/owners","href":"/api-reference/tasks/get-task-owners"},{"title":"Get tasks","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/tasks","href":"/api-reference/tasks/get-tasks"},{"title":"Create task","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/tasks","href":"/api-reference/tasks/create-task"},{"title":"Delete tasks","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/tasks","href":"/api-reference/tasks/delete-tasks"},{"title":"Get task IDs","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/tasks/ids","href":"/api-reference/tasks/get-task-ids"},{"title":"Get task status counts","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/tasks/status-counts","href":"/api-reference/tasks/get-task-status-counts"},{"title":"Get task pending count","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/tasks/pending-count","href":"/api-reference/tasks/get-task-pending-count"},{"title":"Update task","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/tasks/{taskId}","href":"/api-reference/tasks/update-task"},{"title":"Complete task","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/tasks/{taskId}/complete","href":"/api-reference/tasks/complete-task"},{"title":"Complete tasks batch","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/tasks/complete-batch","href":"/api-reference/tasks/complete-tasks-batch"},{"title":"Set skipped tasks","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/tasks/skipped","href":"/api-reference/tasks/set-skipped-tasks"}]},{"group":"Voices","pages":[{"title":"Get voices","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/voices","href":"/api-reference/voices/get-voices"}]},{"group":"Lists","pages":[{"title":"List folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/folders","href":"/api-reference/lists/list-folders"},{"title":"List contact list folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/folders/contacts","href":"/api-reference/lists/list-contact-list-folders"},{"title":"Create contact list folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/folders/contacts","href":"/api-reference/lists/create-contact-list-folder"},{"title":"Get contact list folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/folders/contacts/{folderId}","href":"/api-reference/lists/get-contact-list-folder"},{"title":"Delete contact list folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/folders/contacts/{folderId}","href":"/api-reference/lists/delete-contact-list-folder"},{"title":"Update contact list folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/folders/contacts/{folderId}","href":"/api-reference/lists/update-contact-list-folder"},{"title":"List company list folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/folders/companies","href":"/api-reference/lists/list-company-list-folders"},{"title":"Create company list folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/folders/companies","href":"/api-reference/lists/create-company-list-folder"},{"title":"Get company list folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/folders/companies/{folderId}","href":"/api-reference/lists/get-company-list-folder"},{"title":"Delete company list folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/folders/companies/{folderId}","href":"/api-reference/lists/delete-company-list-folder"},{"title":"Update company list folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/folders/companies/{folderId}","href":"/api-reference/lists/update-company-list-folder"},{"title":"Get lists","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/lists","href":"/api-reference/lists/get-lists"},{"title":"Create a list","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/lists","href":"/api-reference/lists/create-a-list"},{"title":"Delete a list","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/lists/{listId}","href":"/api-reference/lists/delete-a-list"},{"title":"Import a list from AI Finder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/ai-finder/import","href":"/api-reference/lists/import-a-list-from-ai-finder"},{"title":"Add contacts to a list","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/lists/{listId}/contacts","href":"/api-reference/lists/add-contacts-to-a-list"},{"title":"Remove contacts from a list","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/lists/{listId}/contacts","href":"/api-reference/lists/remove-contacts-from-a-list"},{"title":"Move contacts between lists","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/lists/{fromListId}/contacts/move","href":"/api-reference/lists/move-contacts-between-lists"},{"title":"Add companies to a list","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/lists/{listId}/companies","href":"/api-reference/lists/add-companies-to-a-list"},{"title":"Remove companies from a list","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/lists/{listId}/companies","href":"/api-reference/lists/remove-companies-from-a-list"},{"title":"Move companies between lists","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/lists/{fromListId}/companies/move","href":"/api-reference/lists/move-companies-between-lists"}]},{"group":"Saved Filters","pages":[{"title":"Get saved filters","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/saved-filters","href":"/api-reference/saved-filters/get-saved-filters"},{"title":"Create a saved filter","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/saved-filters","href":"/api-reference/saved-filters/create-a-saved-filter"},{"title":"Delete a saved filter","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/saved-filters/{savedFilterId}","href":"/api-reference/saved-filters/delete-a-saved-filter"}]},{"group":"Saved Views","pages":[{"title":"Get saved views","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/saved-views","href":"/api-reference/saved-views/get-saved-views"},{"title":"Get saved view","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/saved-views/{savedViewId}","href":"/api-reference/saved-views/get-saved-view"}]},{"group":"Credits","pages":[{"title":"Get credit balance","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/credits","href":"/api-reference/credits/get-credit-balance"},{"title":"Get credit pricing","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/credits/pricing","href":"/api-reference/credits/get-credit-pricing"}]},{"group":"Actions","pages":[{"title":"Start an actions run","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/actions","href":"/api-reference/actions/start-an-actions-run"},{"title":"Get actions run status","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/actions/{actionsId}/status","href":"/api-reference/actions/get-actions-run-status"}]},{"group":"Inbox","pages":[{"title":"List inbox threads","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/inboxes","href":"/api-reference/inbox/list-inbox-threads"},{"title":"Get inbox contact messages","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/inboxes/:contactId/messages","href":"/api-reference/inbox/get-inbox-contact-messages"},{"title":"Build inbox link","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/inbox/link","href":"/api-reference/inbox/build-inbox-link"},{"title":"List inbox tags","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/inbox/tags","href":"/api-reference/inbox/list-inbox-tags"},{"title":"Create inbox tag","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/inbox/tags","href":"/api-reference/inbox/create-inbox-tag"},{"title":"Get inbox tag","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/inbox/tags/:tagId","href":"/api-reference/inbox/get-inbox-tag"},{"title":"Attach inbox tags to a contact thread","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/inbox/contacts/:contactId/tags","href":"/api-reference/inbox/attach-inbox-tags-to-a-contact-thread"},{"title":"Remove inbox tags from a contact thread","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/inbox/contacts/:contactId/tags","href":"/api-reference/inbox/remove-inbox-tags-from-a-contact-thread"},{"title":"Archive inbox contact thread","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/inboxes/:contactId/archive","href":"/api-reference/inbox/archive-inbox-contact-thread"},{"title":"Unarchive inbox contact thread","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/inboxes/:contactId/unarchive","href":"/api-reference/inbox/unarchive-inbox-contact-thread"},{"title":"Mark inbox contact thread as read","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/inboxes/:contactId/read","href":"/api-reference/inbox/mark-inbox-contact-thread-as-read"},{"title":"Send inbox message","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/inbox/messages","href":"/api-reference/inbox/send-inbox-message"}]},{"group":"Webhooks","pages":[{"title":"List webhook subscriptions","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/webhooks","href":"/api-reference/webhooks/list-webhook-subscriptions"},{"title":"Create webhook subscription","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/webhooks","href":"/api-reference/webhooks/create-webhook-subscription"},{"title":"Get webhook subscription","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/webhooks/{webhookId}","href":"/api-reference/webhooks/get-webhook-subscription"},{"title":"Delete webhook subscription","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/webhooks/{webhookId}","href":"/api-reference/webhooks/delete-webhook-subscription"},{"title":"Update webhook subscription","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/webhooks/{webhookId}","href":"/api-reference/webhooks/update-webhook-subscription"},{"title":"Get webhook delivery logs","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/webhooks/{webhookId}/logs","href":"/api-reference/webhooks/get-webhook-delivery-logs"},{"title":"Test webhook subscription","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/webhooks/{webhookId}/test","href":"/api-reference/webhooks/test-webhook-subscription"},{"title":"Actions completed webhook","version":null,"deprecated":null,"openapi":"https://openapi.enginy.ai/definitions.json webhook /your-webhook-url","href":"/api-reference/webhooks/actions-completed-webhook"},{"title":"Conversation events webhook","version":null,"deprecated":null,"openapi":"https://openapi.enginy.ai/definitions.json webhook /your-conversation-webhook-url","href":"/api-reference/webhooks/conversation-events-webhook"}]},{"group":"Blocklist","pages":[{"title":"List blocklist entries","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/blocklist","href":"/api-reference/blocklist/list-blocklist-entries"},{"title":"Add blocklist entries by value","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/blocklist/items","href":"/api-reference/blocklist/add-blocklist-entries-by-value"},{"title":"Block contacts or companies by ID","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/blocklist/add-entities","href":"/api-reference/blocklist/block-contacts-or-companies-by-id"},{"title":"Delete a blocklist entry","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/blocklist/{itemId}","href":"/api-reference/blocklist/delete-a-blocklist-entry"},{"title":"Update a blocklist entry","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/blocklist/{itemId}","href":"/api-reference/blocklist/update-a-blocklist-entry"},{"title":"Delete blocklist entries in bulk","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/blocklist/bulk-delete","href":"/api-reference/blocklist/delete-blocklist-entries-in-bulk"}]},{"group":"Social Selling","pages":[{"title":"List Social Selling posts","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/social-selling/posts","href":"/api-reference/social-selling/list-social-selling-posts"},{"title":"Track a Social Selling post","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/social-selling/posts","href":"/api-reference/social-selling/track-a-social-selling-post"},{"title":"Get a Social Selling post","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/social-selling/posts/{postId}","href":"/api-reference/social-selling/get-a-social-selling-post"},{"title":"Add reactions to a Social Selling post","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/social-selling/posts/{postId}/reactions","href":"/api-reference/social-selling/add-reactions-to-a-social-selling-post"},{"title":"Get Social Selling best publishing times","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/social-selling/analytics/publishing-times","href":"/api-reference/social-selling/get-social-selling-best-publishing-times"},{"title":"Get Social Selling overview","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/social-selling/analytics/overview","href":"/api-reference/social-selling/get-social-selling-overview"},{"title":"List Social Selling identity performance","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/social-selling/analytics/identities","href":"/api-reference/social-selling/list-social-selling-identity-performance"},{"title":"List Social Selling engagers","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/social-selling/engagers","href":"/api-reference/social-selling/list-social-selling-engagers"},{"title":"List Social Selling post engagers","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/social-selling/posts/{postId}/engagers","href":"/api-reference/social-selling/list-social-selling-post-engagers"},{"title":"Preview Social Selling engager export","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/social-selling/engagers/export-preview","href":"/api-reference/social-selling/preview-social-selling-engager-export"},{"title":"Export Social Selling engagers to a list","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/social-selling/engagers/export","href":"/api-reference/social-selling/export-social-selling-engagers-to-a-list"},{"title":"Add a comment to a Social Selling post","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/social-selling/comments","href":"/api-reference/social-selling/add-a-comment-to-a-social-selling-post"},{"title":"Generate Social Selling comment suggestions","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/social-selling/comment-suggestions","href":"/api-reference/social-selling/generate-social-selling-comment-suggestions"},{"title":"List scheduled Social Selling posts","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/social-selling/scheduled-posts","href":"/api-reference/social-selling/list-scheduled-social-selling-posts"},{"title":"Schedule a Social Selling post","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/social-selling/scheduled-posts","href":"/api-reference/social-selling/schedule-a-social-selling-post"},{"title":"Cancel a scheduled Social Selling post","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/social-selling/scheduled-posts/{publishingPostId}","href":"/api-reference/social-selling/cancel-a-scheduled-social-selling-post"}]},{"group":"Workflows","pages":[{"title":"Get workflow blocks","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflows/blocks","href":"/api-reference/workflows/get-workflow-blocks"},{"title":"Get workflow block inputs","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflows/blocks/{blockId}","href":"/api-reference/workflows/get-workflow-block-inputs"},{"title":"Get workflow condition fields","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflows/condition-fields","href":"/api-reference/workflows/get-workflow-condition-fields"},{"title":"Get workflow import inputs","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflows/import-inputs","href":"/api-reference/workflows/get-workflow-import-inputs"},{"title":"Get workflows","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflows","href":"/api-reference/workflows/get-workflows"},{"title":"Create workflow","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/workflows","href":"/api-reference/workflows/create-workflow"},{"title":"List workflow folders","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflows/folders","href":"/api-reference/workflows/list-workflow-folders"},{"title":"Create workflow folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/workflows/folders","href":"/api-reference/workflows/create-workflow-folder"},{"title":"Delete workflow folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/workflows/folders/{folderId}","href":"/api-reference/workflows/delete-workflow-folder"},{"title":"Update workflow folder","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/workflows/folders/{folderId}","href":"/api-reference/workflows/update-workflow-folder"},{"title":"Validate workflow","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/workflows/validate","href":"/api-reference/workflows/validate-workflow"},{"title":"Get a single workflow","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflow/{workflowId}","href":"/api-reference/workflows/get-a-single-workflow"},{"title":"Delete workflow","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json delete /v1/workflow/{workflowId}","href":"/api-reference/workflows/delete-workflow"},{"title":"Update workflow draft","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json patch /v1/workflow/{workflowId}/draft","href":"/api-reference/workflows/update-workflow-draft"},{"title":"Validate workflow draft","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflow/{workflowId}/validate-draft","href":"/api-reference/workflows/validate-workflow-draft"},{"title":"Publish workflow","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/workflow/{workflowId}/publish","href":"/api-reference/workflows/publish-workflow"},{"title":"Run workflow","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/workflow/{workflowId}/run","href":"/api-reference/workflows/run-workflow"},{"title":"Get workflow runs","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflow/{workflowId}/runs","href":"/api-reference/workflows/get-workflow-runs"},{"title":"Get workflow run status","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/workflow-runs/{runId}","href":"/api-reference/workflows/get-workflow-run-status"},{"title":"Cancel workflow run","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json post /v1/workflow-runs/{runId}/cancel","href":"/api-reference/workflows/cancel-workflow-run"}]},{"group":"Authentication","pages":[{"title":"Validate API key","deprecated":null,"version":null,"openapi":"https://openapi.enginy.ai/definitions.json get /v1/validate","href":"/api-reference/authentication/validate-api-key"}]}]}]},{"tab":"MCP","pages":[{"title":"MCP","href":"/mcp/overview"}]},{"tab":"Workflows","pages":[{"title":"Workflows","href":"/workflows/overview"}]},{"tab":"Custom CRM","pages":[{"title":"Custom CRM","href":"/api-reference/custom-crm/overview"}]}]},"title":"Validate campaign - Enginy API Docs","buildId":"dpl_cadrRc5H9XQiJQbDFN5mcYRs2PAj","deploymentId":"dpl_cadrRc5H9XQiJQbDFN5mcYRs2PAj","dynamics":["c2aa737661","2b790eda92","b0f84f53ff","a9bb46feb3"],"chunks":["OpenApiReferenceProvider-DKJwRUt2.js","utils-CWD_VS3u.js","cloneDeep-izs3Hzxb.js","_Uint8Array-BVIqJ90l.js","isObjectLike-C8vmzPEN.js","isArray-Dcu1dQ5t.js","utils-oFjciODD.js","toString-BX-iyh8G.js","isSymbol-CSH9U2Gm.js","addUniqueKeysToSchema-C76WsxC3.js","snippetPresets-DvPMxpX_.js","style-to-object-BY3JEAdk.js","v4-DIgz8zy6.js","ApiExamples-CuNQfndH.js","isEqual-BfaiBbcl.js","usePlaygroundInputsStore-BVFpVUoF.js","shallow-t59Jg5Kr.js","getApiPlaygroundDisplay-C_JZ4amX.js","CodeSnippet-CLRDnx_h.js","CodeSnippets-C2myU4kc.js","Page-7TwLLapg.js","getPastedJson-B-JaVXw7.js","icon-BA-iDhH3.js","MethodPill-D4qZABFp.js","DuplicateIcon-THIDKO1O.js","createIcon-CYYJwgFd.js","DynamicMarkdown-UGQuxDKp.js","SectionHeading-jYELb23h.js","axios-vUo_bRtp.js","Markdown-DKsLa3wy.js","clsx-CefdjKz2.js","lib--scc4DXi.js","lib-B8HbPgcC.js","html-void-elements-mCSPHezU.js","dist-BaREnp3o.js","katex-YMM-FMeD.js","marked.esm-CZWythHk.js","AssistantPageLink-sgx_0XzF.js"]}