curl --request POST \
--url https://openapi.enginy.ai/v1/campaigns \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"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"
}
]
}
]
}
'{
"status": "success",
"message": "<string>",
"data": {
"id": 123,
"appUrl": "<string>",
"name": "<string>",
"status": "DRAFT",
"identityId": 123,
"warnings": [
"<string>"
]
}
}Create campaign
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.
Responses 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.
Create a draft campaign from the public steps format.
How to structure the request:
- Start
stepswith the first action that should happen. - Keep adding the next action in the order it should run.
- Use
conditionwhen you want to branch on lead data. - Use
linkedin_connectionwithwaitForAcceptancewhen you want to branch on connection acceptance after sending a connection request. OmitwaitForAcceptancefor a fire-and-forget connection request. - Use
linkedin_messagewithcontent,attachment, or both. Attachment-only messages are valid. - Use
whatsapp_messageonly 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.
AI-generated messages (message library):
email,linkedin_message,linkedin_inmail, andlinkedin_connectionsteps acceptaiMessageIdinstead of manualcontent— 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-messagesand pass the entryidas the stepaiMessageId. The step gets its own copy automatically. - To reuse or create a workspace AI message, use
GET /v1/ai-variables/ai-messagesorPOST /v1/ai-variables/ai-messages(full control overprompt,toneId,model,outputLength, andsplitMessages), then pass itsidasaiMessageId. - The AI message
channelmust match the step type (EMAIL,LINKEDIN,LINKEDIN_INMAIL, orLINKEDIN_CONNECTION). - Never combine
aiMessageIdwith manualcontentorsubjecton the same step — foremailandlinkedin_inmailthe AI message generates the subject too.
Before you send a request:
- Send an ordered
stepsarray that describes the campaign flow from first action to last action. - Use branch arrays on
conditionandlinkedin_connectionsteps to describe branch-specific work. - Place shared follow-up steps after a branching step when both live branches should continue together.
waitForAcceptance.unitmust bedaysandwaitForAcceptance.valuemust be an integer greater than or equal to1.- Use
GET /v1/tasks/ownersfirst when your campaign includestasksteps. If owners are returned, everytaskstep must includeownerId. - Validation errors use full paths like
steps[0].onTrue[1].subjectso 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 thestepsreturned byGET /v1/campaign/{campaignId}. Either way the read-onlystepId,connectionId,nextConnectionId, andadvancedfields are accepted and ignored, but fill in messages that come back empty (their content is AI-generated per contact — recreate them withaiMessageIdwhere the step type supports it, or provide manualcontent).
Supported condition.type values:
has_linkedin_profile:onTrueruns when the lead has a LinkedIn profile.has_professional_email:onTrueruns when the lead has a professional email.is_already_connected:onTrueruns when the lead is already a LinkedIn connection.has_been_contacted:onTrueruns when the lead has already been contacted.has_whatsapp_account:onTrueruns when the lead has a WhatsApp account.email_opened:onTrueruns when the lead opens the preceding email withinwaitFor. Must follow anemailstep. RequireswaitFor(value,unit).email_clicked:onTrueruns when the lead clicks a link in the preceding email withinwaitFor. Must follow anemailstep. RequireswaitFor(value,unit).linkedin_message_viewed:onTrueruns when the lead views the preceding LinkedIn message withinwaitFor. Must follow a LinkedIn message step (linkedin_message,linkedin_message_bundle,linkedin_voice_message, orlinkedin_inmail). RequireswaitFor(value,unit).task_completed:onTrueruns when the precedingtaskstep is marked complete withinwaitFor. Must follow ataskstep. RequireswaitFor(value,unit).connection_accepted:onTrueruns when the lead accepts a previous LinkedIn connection request withinwaitFor. RequireswaitFor(value,unit).lead_field:onTrueruns when the contactfieldmatches theoperator(andvalue, where applicable). Use this to branch on any built-in column or custom field. Requiresfieldandoperator.valueis required for every operator exceptIS_EMPTYandIS_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:
conditionusesonTrueandonFalse.linkedin_connectionusesonAcceptedandonNotAcceptedonly whenwaitForAcceptanceis set.- Use
[{ "type": "end" }]when a branch should stop explicitly.
Routing into another campaign:
- Use
add_to_another_campaignwith acampaignIdto route the lead into an existing campaign in the same workspace. - The referenced
campaignIdmust belong to the same client as the API key. If you are cloning campaigns across workspaces, remap eachcampaignIdto 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_WRITERate limit: 30 requests per minute
curl --request POST \
--url https://openapi.enginy.ai/v1/campaigns \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"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"
}
]
}
]
}
'{
"status": "success",
"message": "<string>",
"data": {
"id": 123,
"appUrl": "<string>",
"name": "<string>",
"status": "DRAFT",
"identityId": 123,
"warnings": [
"<string>"
]
}
}Authorizations
Body
Campaign name.
1Ordered campaign steps. Use branch arrays like onTrue, onFalse, onAccepted, and onNotAccepted for branching. Steps returned by GET /v1/campaign/{campaignId} can be resubmitted: the read-only stepId, connectionId, nextConnectionId, and advanced fields are accepted and ignored.
1- email
- linkedin_message
- linkedin_message_bundle
- linkedin_voice_message
- linkedin_inmail
- linkedin_visit_profile
- linkedin_like_last_post
- whatsapp_message
- condition
- linkedin_connection
- task
- add_to_another_campaign
- end
Show child attributes
Show child attributes
Optional campaign description.
Allowed sender email addresses for this campaign.