> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enginy.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaign Creation Guide

> How to structure POST /v1/campaigns requests with steps, branches, and task owners

## Overview

Use `POST /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

1. Start `steps` with the first action that should happen.
2. Add each next action in the exact order it should run.
3. Use `condition` to branch on lead data.
4. Use `linkedin_connection` to send a connection request and then branch on acceptance.
5. Put shared continuation steps after the branch instead of duplicating them inside both sides.

## Rules

* Build the campaign as an ordered `steps` array from first action to last action.
* Use `condition` and `linkedin_connection` when 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.unit` must always be `days`.
* `waitForAcceptance.value` must be an integer greater than or equal to `1`.
* Validation errors return full paths such as `steps[0].onTrue[1].subject`.

## Step Types

| Step type                 | Required fields                       | Optional fields                                                        |
| ------------------------- | ------------------------------------- | ---------------------------------------------------------------------- |
| `email`                   | `content`                             | `subject`, `delay`                                                     |
| `linkedin_message`        | `content` *or* `attachment`           | `attachment`, `attachmentName`, `contentType`, `delay`                 |
| `linkedin_message_bundle` | `messages`                            | `delay`                                                                |
| `linkedin_voice_message`  | `content`+`voiceId` *or* `attachment` | `voiceSettings`, `delay`                                               |
| `linkedin_inmail`         | `subject`, `content`                  | `delay`                                                                |
| `linkedin_visit_profile`  | None                                  | `delay`                                                                |
| `linkedin_like_last_post` | None                                  | `delay`, `reactionType`                                                |
| `whatsapp_message`        | `content`                             | `delay`                                                                |
| `condition`               | `condition`                           | `onTrue`, `onFalse`                                                    |
| `linkedin_connection`     | None                                  | `waitForAcceptance`, `delay`, `content`, `onAccepted`, `onNotAccepted` |
| `task`                    | `taskType`, `title`                   | `note`, `ownerId`, `delay`                                             |
| `add_to_another_campaign` | `campaignId`                          | `delay`                                                                |
| `end`                     | None                                  | None                                                                   |

> `email.subject` is required for the first email on every live path; follow-up emails in the same thread can omit it. `linkedin_connection.waitForAcceptance` is 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.campaignId` must reference a campaign owned by the same client as the API key.

### `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:

| Field            | Type                                      | Notes                                                                   |
| ---------------- | ----------------------------------------- | ----------------------------------------------------------------------- |
| `content`        | `string`                                  | Optional text body. At least one of `content` or `attachment` required. |
| `attachment`     | `string`                                  | Optional URL or storage key of a file to send.                          |
| `attachmentName` | `string`                                  | Optional display name for the attachment.                               |
| `contentType`    | `"AUDIO" \| "IMAGE" \| "FILE" \| "VIDEO"` | Optional. Recommended when `attachment` is set.                         |

### `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).

| Field            | Type      | Notes                                                                                          |
| ---------------- | --------- | ---------------------------------------------------------------------------------------------- |
| `content`        | `string`  | Optional. Text that will be synthesized to speech using the configured `voiceId`.              |
| `voiceId`        | `string`  | Optional. ID of a system voice or an identity-cloned voice. Discoverable via `GET /v1/voices`. |
| `attachment`     | `string`  | Optional. URL or storage key of a pre-recorded audio file to send instead of synthesized text. |
| `attachmentName` | `string`  | Optional. Display name for the audio attachment.                                               |
| `contentType`    | `"AUDIO"` | Optional. Recommended when `attachment` is set.                                                |
| `voiceSettings`  | `object`  | Optional. Fine-tuning of the synthesized voice and background audio (see below).               |

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):

| Field              | Type     | Notes                                                                               |
| ------------------ | -------- | ----------------------------------------------------------------------------------- |
| `voiceSpeed`       | `number` | Speech rate multiplier. Range `0.5`–`2.0`.                                          |
| `voiceStability`   | `number` | Voice stability percentage. Range `0`–`100`. Higher is steadier.                    |
| `backgroundNoise`  | `string` | Background noise preset (e.g. `none`, `office`).                                    |
| `backgroundVolume` | `number` | Background noise volume. Range `0`–`100`. Ignored when `backgroundNoise` is `none`. |

## Branching Step Types

| Use this step         | When you need it                                         | Branch fields                 |
| --------------------- | -------------------------------------------------------- | ----------------------------- |
| `condition`           | Branch based on what Enginy already knows about the lead | `onTrue`, `onFalse`           |
| `linkedin_connection` | Send a connection request and branch based on acceptance | `onAccepted`, `onNotAccepted` |

## Condition Types

`condition.type` supports exactly these values:

| Condition type           | Extra fields                          | `onTrue` branch runs when                                                | `onFalse` branch runs when                               |
| ------------------------ | ------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------- |
| `has_linkedin_profile`   | –                                     | The lead has a LinkedIn profile                                          | The lead does not have a LinkedIn profile                |
| `has_professional_email` | –                                     | The lead has a professional email                                        | The lead does not have a professional email              |
| `is_already_connected`   | –                                     | The lead is already a LinkedIn connection                                | The lead is not already a LinkedIn connection            |
| `has_been_contacted`     | –                                     | The lead has already been contacted                                      | The lead has not been contacted yet                      |
| `has_whatsapp_account`   | –                                     | The lead has a WhatsApp account                                          | The lead does not have a WhatsApp account                |
| `email_opened`           | `waitFor` (required)                  | The lead opens the preceding email within `waitFor`                      | `waitFor` elapses without the email being opened         |
| `email_clicked`          | `waitFor` (required)                  | The lead clicks a link in the preceding email within `waitFor`           | `waitFor` elapses without a link being clicked           |
| `task_completed`         | `waitFor` (required)                  | The preceding `task` step is marked complete within `waitFor`            | `waitFor` elapses without the task being marked complete |
| `connection_accepted`    | `waitFor` (required)                  | The lead accepts an earlier LinkedIn connection request within `waitFor` | `waitFor` elapses without the connection being accepted  |
| `lead_field`             | `field`, `operator`, `value` (mostly) | The contact's `field` matches `operator` (and `value`, where applicable) | The match fails                                          |

`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`, `task_completed`, and `connection_accepted` require a `waitFor` object on the condition:

```json theme={null}
{
  "type": "email_opened",
  "waitFor": { "value": 3, "unit": "days" }
}
```

* `waitFor.value` must be a positive number.
* `waitFor.unit` must be one of `seconds`, `minutes`, `hours`, or `days`.
* `email_opened` and `email_clicked` must be placed immediately after an `email` step.
* `task_completed` must be placed immediately after a `task` step.
* `connection_accepted` checks 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 the `linkedin_connection` step itself.

## Validation Checklist

* `steps` must 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.unit` must always be `days`.
* `waitForAcceptance.value` must be a whole integer greater than or equal to `1`.
* If `GET /v1/tasks/owners` returns owners, every `task` step must include a valid `ownerId`.
* Every `linkedin_voice_message` step must reference a `voiceId` returned by `GET /v1/voices`.

## Task Owners

If your campaign contains `task` steps, call `GET /v1/tasks/owners` first.

* If that endpoint returns owners, every `task` step must include a valid `ownerId`.
* If no owners are returned, `ownerId` can be omitted.

## Voices

If your campaign contains `linkedin_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 `voiceId` that is not in the list will fail validation when creating or validating the campaign.

## Related Endpoints

* `POST /v1/campaigns` creates the draft campaign.
* `GET /v1/campaigns` lists campaigns after creation.
* `GET /v1/tasks/owners` returns the valid task owners for `task` steps.
* `GET /v1/voices` returns the valid voices for `linkedin_voice_message` steps.
* `POST /v1/add-contact-to-campaign` adds one contact to an existing campaign.
* `POST /v1/add-contact-group-to-campaign` adds 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-campaign` is for one contact at a time.
* `POST /v1/add-contact-group-to-campaign` is for an entire contact group and returns counts for newly added, reactivated, already-present, and skipped contacts.

### Example: Add a Contact Group to a Campaign

```json theme={null}
{
  "campaignId": 456,
  "contactGroupId": 123
}
```

## Example: Condition First

```json theme={null}
{
  "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"
        }
      ]
    }
  ]
}
```

## Example: LinkedIn Acceptance Branch

```json theme={null}
{
  "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"
      }
    }
  ]
}
```

## Example: LinkedIn Message Bundle

Send a short text message followed by a file attachment in the same LinkedIn step.

```json theme={null}
{
  "name": "LinkedIn bundle intro",
  "identityId": 323,
  "steps": [
    {
      "type": "linkedin_message_bundle",
      "delay": {
        "value": 2,
        "unit": "hours"
      },
      "messages": [
        {
          "content": "Hi {firstName}, great to connect! Sending along a short intro."
        },
        {
          "content": "Here is the deck we discussed.",
          "attachment": "https://cdn.example.com/deck.pdf",
          "attachmentName": "intro-deck.pdf",
          "contentType": "FILE"
        }
      ]
    }
  ]
}
```

## Example: Branch on Email Click

Follow up on LinkedIn only if the lead clicks the tracked email link within three days, otherwise bump via email.

```json theme={null}
{
  "name": "Click-gated follow-up",
  "identityId": 323,
  "steps": [
    {
      "type": "email",
      "subject": "Thought you might find this useful",
      "content": "Here is the landing page with the details."
    },
    {
      "type": "condition",
      "condition": {
        "type": "email_clicked",
        "waitFor": {
          "value": 3,
          "unit": "days"
        }
      },
      "onTrue": [
        {
          "type": "linkedin_message",
          "content": "Saw you checked out the page. Happy to jump on a quick call."
        }
      ],
      "onFalse": [
        {
          "type": "email",
          "subject": "Still interested?",
          "content": "Quick bump in case this got buried."
        }
      ]
    }
  ]
}
```

## Example: Task Completion Branch

Wait up to five days for a task to be marked complete; otherwise fall back to another channel.

```json theme={null}
{
  "name": "Task completion fallback",
  "identityId": 323,
  "steps": [
    {
      "type": "task",
      "taskType": "CALL",
      "title": "Initial discovery call",
      "ownerId": "owner-1"
    },
    {
      "type": "condition",
      "condition": {
        "type": "task_completed",
        "waitFor": {
          "value": 5,
          "unit": "days"
        }
      },
      "onTrue": [
        {
          "type": "email",
          "subject": "Great talking to you",
          "content": "Following up with a quick summary of what we discussed."
        }
      ],
      "onFalse": [
        {
          "type": "linkedin_message",
          "content": "Hey {firstName}, still keen to chat whenever works for you."
        }
      ]
    }
  ]
}
```

## Example: Explicitly Stop One Branch

```json theme={null}
{
  "name": "Accepted-or-stop flow",
  "identityId": 323,
  "steps": [
    {
      "type": "linkedin_connection",
      "content": "Would love to connect and share a few ideas.",
      "waitForAcceptance": {
        "value": 3,
        "unit": "days"
      },
      "onAccepted": [
        {
          "type": "linkedin_message",
          "content": "Thanks for accepting. Sharing a quick follow-up.",
          "delay": {
            "value": 4,
            "unit": "hours"
          }
        }
      ],
      "onNotAccepted": [
        {
          "type": "end"
        }
      ]
    },
    {
      "type": "email",
      "subject": "Resources after connecting",
      "content": "Continuing only for contacts who accepted the connection request.",
      "delay": {
        "value": 1,
        "unit": "days"
      }
    }
  ]
}
```
