Overview
UsePOST /v1/campaigns to create a draft campaign from the public steps format.
This endpoint is designed for external integrations. You send a single ordered steps array and Enginy compiles it into the internal campaign graph.
Quick Start
- Start
stepswith the first action that should happen. - Add each next action in the exact order it should run.
- Use
conditionto branch on lead data. - Use
linkedin_connectionto send a connection request and then branch on acceptance. - Put shared continuation steps after the branch instead of duplicating them inside both sides.
Rules
- Build the campaign as an ordered
stepsarray from first action to last action. - Use
conditionandlinkedin_connectionwhen you need branches. - Put shared follow-up work after the branching step instead of duplicating the same steps in both branches.
- Use
{ "type": "end" }only when that branch should stop completely. waitForAcceptance.unitmust always bedays.waitForAcceptance.valuemust be an integer greater than or equal to1.- Validation errors return full paths such as
steps[0].onTrue[1].subject.
Step Types
email.subjectis required for the first email on every live path; follow-up emails in the same thread can omit it.linkedin_connection.waitForAcceptanceis optional — omit it (and the branches) for a fire-and-forget connection request that just sends the invite and continues with whatever follows.add_to_another_campaign.campaignIdmust reference a campaign owned by the same client as the API key.
AI messages on steps (aiMessageId)
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 (for email and linkedin_inmail it generates the subject too, so omit subject).
- Reuse a public prompt-library entry: list them with
GET /v1/prompt-library/ai-messagesand pass the entryid. - Reuse a workspace AI message: list them with
GET /v1/ai-variables/ai-messages. - Create a new AI message with full control over
prompt,toneId,model,outputLength, andsplitMessages:POST /v1/ai-variables/ai-messages(tones viaGET /v1/ai-variables/ai-message-tones), then reference the returnedid.
- The AI message
channelmust match the step type:EMAIL,LINKEDIN,LINKEDIN_INMAIL, orLINKEDIN_CONNECTION. aiMessageIdis mutually exclusive with manualcontent,subject, and attachment fields — the AI message replaces the step’s message entirely.- Each step receives its own copy of the AI message; campaign reads return the copy’s id.
- Requires the AI variable split to be enabled for the workspace.
Personalization placeholders
Manualcontent/subject accepts single-brace placeholders that must reference real fields — discover them with GET /v1/contacts/fields. Examples: {firstName}, {companyName}, {identity.name}. Unknown placeholders are rejected with a 422. The company aliases {company}, {companyName}, and {companyDomain} are accepted and stored as the canonical company columns ({name}, {domain}).
linkedin_message_bundle
Sends several LinkedIn messages back-to-back in the same step. Each entry in messages can carry text, an attachment, or both:
linkedin_voice_message
Sends a LinkedIn voice message. You can either generate audio from text via text-to-speech (content + voiceId) or provide a pre-recorded audio attachment (attachment with contentType: "AUDIO"). Use GET /v1/voices to discover valid voiceId values (system voices and identity-cloned voices owned by the client).
The step must satisfy one of: (a) both
content and voiceId are provided so we synthesize speech, or (b) attachment is provided to send a pre-recorded audio file. voiceSettings only applies in mode (a).
voiceSettings fields (all optional):
Branching Step Types
Condition Types
condition.type supports exactly these values:
lead_field lets you branch on any built-in column or custom field. operator accepts: EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS, IS_EMPTY, IS_NOT_EMPTY. value is required for every operator except IS_EMPTY and IS_NOT_EMPTY.
Timed Conditions (waitFor)
email_opened, email_clicked, linkedin_message_viewed, task_completed, and connection_accepted require a waitFor object on the condition:
waitFor.valuemust be a positive number.waitFor.unitmust be one ofseconds,minutes,hours, ordays.email_openedandemail_clickedmust be placed immediately after anemailstep.linkedin_message_viewedmust be placed immediately after a LinkedIn message step (linkedin_message,linkedin_message_bundle,linkedin_voice_message, orlinkedin_inmail).task_completedmust be placed immediately after ataskstep.connection_acceptedchecks whether the lead has accepted a LinkedIn connection request that was sent earlier in the campaign. Use this when you want to fan out work after the request without inlining the acceptance check on thelinkedin_connectionstep itself.
Validation Checklist
stepsmust contain at least one step.- Every branch array you provide must contain at least one step.
- Use
{ "type": "end" }if a branch should stop explicitly. waitForAcceptance.unitmust always bedays.waitForAcceptance.valuemust be a whole integer greater than or equal to1.- If
GET /v1/tasks/ownersreturns owners, everytaskstep must include a validownerId. - Every
linkedin_voice_messagestep must reference avoiceIdreturned byGET /v1/voices. - Every
aiMessageIdmust reference a workspace AI message or a prompt-library entry whosechannelmatches the step type, and must not be combined with manualcontent/subject. - Placeholders in manual content must reference fields returned by
GET /v1/contacts/fields(unknown placeholders are rejected). - The sequence must not loop: a step cannot connect back to an earlier step. A cyclic campaign is rejected instead of created (it could be stored before, but never ran — the sending engine requires an acyclic sequence).
Task Owners
If your campaign containstask steps, call GET /v1/tasks/owners first.
- If that endpoint returns owners, every
taskstep must include a validownerId. - If no owners are returned,
ownerIdcan be omitted.
Voices
If your campaign containslinkedin_voice_message steps, call GET /v1/voices to discover the available voiceId values:
source: "system"voices are available to every client.source: "identity"voices are cloned from one of the client’s identities.- A
voiceIdthat is not in the list will fail validation when creating or validating the campaign.
Updating a Draft Campaign
UsePATCH /v1/campaign/{campaignId} to modify a campaign that is still in DRAFT status — both its settings (name, description, identity, tracking flags, …) and its step sequence.
- Every field is optional; provide only what you want to change.
- When
stepsis provided it replaces the whole stored sequence. Fetch the current steps withGET /v1/campaign/{campaignId}, modify them, and resubmit — the read-onlystepId,connectionId,nextConnectionId, andadvancedfields that GET emits are accepted and ignored, and rich-text formatting of steps whose text you did not change is preserved automatically. - Reply follow-up emails may keep the
subject: ""that GET returns; it means “reply in the existing thread”. - Campaigns that are not in
DRAFTstatus return409 Conflict: clone them (POST /v1/campaign/{campaignId}/clone) and edit the clone instead. A draft that already has send history from a previous launch also returns409for step replacement.
Related Endpoints
POST /v1/campaignscreates the draft campaign.PATCH /v1/campaign/{campaignId}updates a draft campaign’s settings and/or steps.GET /v1/campaignslists campaigns after creation.GET /v1/tasks/ownersreturns the valid task owners fortasksteps.GET /v1/voicesreturns the valid voices forlinkedin_voice_messagesteps.POST /v1/add-contact-to-campaignadds one contact to an existing campaign.POST /v1/add-contact-group-to-campaignadds every contact in a contact group to an existing campaign.
Adding Contacts After Creation
Use the campaign creation endpoint to build the sequence first, then use one of the audience endpoints below to start adding contacts to it.POST /v1/add-contact-to-campaignis for one contact at a time.POST /v1/add-contact-group-to-campaignis for an entire contact group and returns counts for newly added, reactivated, already-present, and skipped contacts.