# Replace the sequence

Source: https://docs.emelia.io/docs/campaigns/replace-sequence

Guide: [Campaigns](https://docs.emelia.io/docs/campaigns.md)

`PATCH https://api.emelia.io/advanced/campaigns/{campaignId}/steps`

Replaces the WHOLE sequence tree of the campaign. Always `GET` the campaign first, modify the `steps` object and send the complete tree back. The campaign must not be RUNNING (pause it first). Missing step and version `_id`s are generated by the server; keep existing ids on a campaign that already ran, otherwise history, statistics and the position of contacts in the sequence are lost and contacts may receive steps again. Content is not validated here: empty email versions, missing subjects or audio are only reported by `POST .../start`.

## Authentication

Send your API key in the `Authorization` header: `Authorization: <your API key>`.

## Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `campaignId` | string | yes | Identifier of a multichannel campaign (24 hexadecimal characters), as returned by `POST /advanced/campaigns` or `GET /advanced/campaigns`. |

## Request body

Content type: `application/json`, required.

| Field | Type | Required | Description |
|---|---|---|---|
| `steps` | object | yes | One node of the sequence tree. The root is always a `START` step. Each node points to the next one with `next`; a `CONDITION` node branches with `yes` and `no` (a missing branch ends the sequence for the contacts that fall into it). `END_OF_CAMPAIGN` stops the sequence. EMAIL versions are A/B variants (one version per contact); MESSAGE and WHATSAPP versions are successive messages sent in order. A MESSAGE step placed after a CONNECTION step waits for the invitation to be accepted. |
| `steps._id` | string | no | Step identifier, generated by the server (ObjectId string) when missing; the root is `"START"`. Any unique string is accepted. Never regenerate the ids of a campaign that already ran. |
| `steps.stepType` | enum: `START`, `EMAIL`, `CONNECTION`, `MESSAGE`, `INMAIL`, `VISIT`, `LIKE`, `FOLLOW`, `AUDIO`, `WHATSAPP`, `TASK`, `CONDITION`, `API_CALL`, `END_OF_CAMPAIGN` | yes | `FOLLOW`, `AUDIO` and `API_CALL` are accepted when saved, but nothing runs them: no action happens and the step is never marked as done. Do not use them. A LinkedIn voice note is a `MESSAGE` step whose first version carries an attachment with `mediaType: "AUDIO"`, uploaded with `POST /advanced/linkedin/audio`. |
| `steps.delay` | object | yes | Wait time after the previous action before this step runs. The delay of the first action of the sequence is ignored (new contacts enter according to the daily quotas). With `DAYS`, days that are not in `schedule.days` are skipped. On a `CONDITION` step the delay is the observation window. |
| `steps.delay.amount` | number | yes | Range: 0 to .... |
| `steps.delay.unit` | enum: `MINUTES`, `HOURS`, `DAYS` | yes |  |
| `steps.identities` | array of string | no | Legacy field, not read when sending. Send `[]`. Sending accounts are chosen automatically among the campaign identities. |
| `steps.versions` | array of object | no | Content of EMAIL, CONNECTION, MESSAGE, INMAIL, WHATSAPP and TASK steps. |
| `steps.versions[]._id` | string | no | Version identifier. Generated by the server (ObjectId string) when missing. Keep existing ids when you update a campaign that already ran: statistics and history are attached to them. |
| `steps.versions[].subject` | string | no | EMAIL: subject. Leave it empty on a follow up email to reply in the same thread ("Re: previous subject"). INMAIL: subject. TASK: the task label shown in manual tasks. |
| `steps.versions[].message` | string | no | EMAIL: HTML body. MESSAGE, INMAIL, CONNECTION (invitation note) and WHATSAPP: plain text. Supports `{{variable}}` placeholders and `{a\|b\|c}` spintax. |
| `steps.versions[].disabled` | boolean | no | Excludes the version from A/B rotation (EMAIL). |
| `steps.versions[].rawHtml` | boolean | no | Stored but not read by the multichannel sending path. |
| `steps.versions[].attachments` | array of object | no |  |
| `steps.cc` | string | no | EMAIL only: carbon copy address, may be a variable such as `{{assistantEmail}}`; ignored when the resolved value is not a valid email. |
| `steps.conditions` | object | no |  |
| `steps.conditions.field` | string | yes | Either an activity condition in UPPER CASE (`ACCEPTED`, `LINKEDIN_REPLIED`, `MAIL_REPLIED`, `OPENED`, `CLICKED`, `UNSUBSCRIBED`, `BOUNCED`, `TASK_COMPLETED`), evaluated within the step delay window, or a contact field prefixed with `$` (`$firstName`, `$jobTitle`, `$myCustomVariable`), evaluated immediately. |
| `steps.conditions.operator` | enum: `EQUAL`, `DOES_NOT_EQUAL`, `GREATER`, `LOWER`, `CONTAINS`, `DOES_NOT_CONTAIN`, `IS_EMPTY`, `IS_NOT_EMPTY` | yes | Only these values are evaluated by the scheduler; any other value (for example `NOT_EQUAL` or `GREATER_THAN`) always evaluates to false. For activity conditions use `EQUAL` with value `"true"`. |
| `steps.conditions.value` | string | yes |  |
| `steps.next` | object | no | (recursive) |
| `steps.yes` | object | no | (recursive) |
| `steps.no` | object | no | (recursive) |

```json
{
  "steps": {
    "_id": "START",
    "stepType": "START",
    "identities": [],
    "delay": {
      "amount": 0,
      "unit": "DAYS"
    },
    "next": {
      "stepType": "CONNECTION",
      "identities": [],
      "delay": {
        "amount": 0,
        "unit": "DAYS"
      },
      "versions": [
        {
          "message": "Hi {{firstName}}, glad to connect."
        }
      ],
      "next": {
        "stepType": "CONDITION",
        "identities": [],
        "delay": {
          "amount": 3,
          "unit": "DAYS"
        },
        "conditions": {
          "field": "ACCEPTED",
          "operator": "EQUAL",
          "value": "true"
        },
        "yes": {
          "stepType": "MESSAGE",
          "identities": [],
          "delay": {
            "amount": 1,
            "unit": "HOURS"
          },
          "versions": [
            {
              "message": "Thanks for connecting {{firstName}}!"
            }
          ]
        },
        "no": {
          "stepType": "EMAIL",
          "identities": [],
          "delay": {
            "amount": 2,
            "unit": "HOURS"
          },
          "versions": [
            {
              "subject": "Quick question, {{firstName}}",
              "message": "<p>Hi {{firstName}},</p><p>I sent you an invitation on LinkedIn...</p>"
            }
          ],
          "next": {
            "stepType": "END_OF_CAMPAIGN",
            "identities": [],
            "delay": {
              "amount": 0,
              "unit": "DAYS"
            }
          }
        }
      }
    }
  }
}
```

## Example request

**cURL**

```bash
curl -X PATCH "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/steps" \
  -H "Authorization: $EMELIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "steps": {
    "_id": "START",
    "stepType": "START",
    "identities": [],
    "delay": {
      "amount": 0,
      "unit": "DAYS"
    },
    "next": {
      "stepType": "CONNECTION",
      "identities": [],
      "delay": {
        "amount": 0,
        "unit": "DAYS"
      },
      "versions": [
        {
          "message": "Hi {{firstName}}, glad to connect."
        }
      ],
      "next": {
        "stepType": "CONDITION",
        "identities": [],
        "delay": {
          "amount": 3,
          "unit": "DAYS"
        },
        "conditions": {
          "field": "ACCEPTED",
          "operator": "EQUAL",
          "value": "true"
        },
        "yes": {
          "stepType": "MESSAGE",
          "identities": [],
          "delay": {
            "amount": 1,
            "unit": "HOURS"
          },
          "versions": [
            {
              "message": "Thanks for connecting {{firstName}}!"
            }
          ]
        },
        "no": {
          "stepType": "EMAIL",
          "identities": [],
          "delay": {
            "amount": 2,
            "unit": "HOURS"
          },
          "versions": [
            {
              "subject": "Quick question, {{firstName}}",
              "message": "<p>Hi {{firstName}},</p><p>I sent you an invitation on LinkedIn...</p>"
            }
          ],
          "next": {
            "stepType": "END_OF_CAMPAIGN",
            "identities": [],
            "delay": {
              "amount": 0,
              "unit": "DAYS"
            }
          }
        }
      }
    }
  }
}'
```

**JavaScript**

```js
const response = await fetch('https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/steps', {
  method: 'PATCH',
  headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "steps": {
      "_id": "START",
      "stepType": "START",
      "identities": [],
      "delay": {
        "amount": 0,
        "unit": "DAYS"
      },
      "next": {
        "stepType": "CONNECTION",
        "identities": [],
        "delay": {
          "amount": 0,
          "unit": "DAYS"
        },
        "versions": [
          {
            "message": "Hi {{firstName}}, glad to connect."
          }
        ],
        "next": {
          "stepType": "CONDITION",
          "identities": [],
          "delay": {
            "amount": 3,
            "unit": "DAYS"
          },
          "conditions": {
            "field": "ACCEPTED",
            "operator": "EQUAL",
            "value": "true"
          },
          "yes": {
            "stepType": "MESSAGE",
            "identities": [],
            "delay": {
              "amount": 1,
              "unit": "HOURS"
            },
            "versions": [
              {
                "message": "Thanks for connecting {{firstName}}!"
              }
            ]
          },
          "no": {
            "stepType": "EMAIL",
            "identities": [],
            "delay": {
              "amount": 2,
              "unit": "HOURS"
            },
            "versions": [
              {
                "subject": "Quick question, {{firstName}}",
                "message": "<p>Hi {{firstName}},</p><p>I sent you an invitation on LinkedIn...</p>"
              }
            ],
            "next": {
              "stepType": "END_OF_CAMPAIGN",
              "identities": [],
              "delay": {
                "amount": 0,
                "unit": "DAYS"
              }
            }
          }
        }
      }
    }
  }),
});
console.log(await response.json());
```

**Python**

```python
import os
import requests

response = requests.patch(
    "https://api.emelia.io/advanced/campaigns/6650f1a2b839d80011c5a0f1/steps",
    headers={"Authorization": os.environ["EMELIA_API_KEY"]},
    json={
        "steps": {
            "_id": "START",
            "stepType": "START",
            "identities": [],
            "delay": {
                "amount": 0,
                "unit": "DAYS"
            },
            "next": {
                "stepType": "CONNECTION",
                "identities": [],
                "delay": {
                    "amount": 0,
                    "unit": "DAYS"
                },
                "versions": [
                    {
                        "message": "Hi {{firstName}}, glad to connect."
                    }
                ],
                "next": {
                    "stepType": "CONDITION",
                    "identities": [],
                    "delay": {
                        "amount": 3,
                        "unit": "DAYS"
                    },
                    "conditions": {
                        "field": "ACCEPTED",
                        "operator": "EQUAL",
                        "value": "True"
                    },
                    "yes": {
                        "stepType": "MESSAGE",
                        "identities": [],
                        "delay": {
                            "amount": 1,
                            "unit": "HOURS"
                        },
                        "versions": [
                            {
                                "message": "Thanks for connecting {{firstName}}!"
                            }
                        ]
                    },
                    "no": {
                        "stepType": "EMAIL",
                        "identities": [],
                        "delay": {
                            "amount": 2,
                            "unit": "HOURS"
                        },
                        "versions": [
                            {
                                "subject": "Quick question, {{firstName}}",
                                "message": "<p>Hi {{firstName}},</p><p>I sent you an invitation on LinkedIn...</p>"
                            }
                        ],
                        "next": {
                            "stepType": "END_OF_CAMPAIGN",
                            "identities": [],
                            "delay": {
                                "amount": 0,
                                "unit": "DAYS"
                            }
                        }
                    }
                }
            }
        }
    },
)
print(response.json())
```

## Responses

### 200: Success.

| Field | Type | Required | Description |
|---|---|---|---|
| `success` | boolean | yes |  |

```json
{
  "success": true
}
```

### 400: Invalid request.

| Field | Type | Required | Description |
|---|---|---|---|
| `error` | string or object | yes | Human readable message. A few legacy routes return an empty object `{}` here instead of a string. |
| `code` | enum: `INVALID_REQUEST`, `INVALID_PARAMETERS`, `SUBSCRIPTION_REQUIRED`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_ALREADY_RUNNING`, `SCHEDULE_MISSING`, `STEPS_MISSING`, `NO_RECIPIENTS`, `TOO_MANY_RECIPIENTS`, `PLANNED_START_INVALID`, `PLANNED_START_IN_PAST`, `EMAIL_PROVIDER_MISSING`, `EMAIL_PROVIDER_DISCONNECTED`, `EMAIL_PROVIDER_EXPIRED`, `ONE_EMAIL_PROVIDER_EXPIRED`, `EMAIL_ADDRESS_DISCONNECTED`, `TRACKING_DOMAINS_REQUIRED`, `LINKEDIN_ACCOUNT_MISSING`, `LINKEDIN_COOKIE_EXPIRED`, `LINKEDIN_SLOT_EXPIRED`, `WHATSAPP_ACCOUNT_EXPIRED`, `WHATSAPP_SLOT_EXPIRED`, `NO_IDENTITY_SET`, `IDENTITY_FOREIGN_ACCOUNT`, `IDENTITY_EMAIL_PROVIDER_MISSING`, `LINKEDIN_ACCOUNT_AMBIGUOUS`, `WHATSAPP_ACCOUNT_AMBIGUOUS`, `LINKEDIN_AUDIO_MISSING`, `EMAIL_STEP_NO_ACTIVE_VERSION`, `EMAIL_VERSION_NO_CONTENT`, `EMAIL_VERSION_NO_SUBJECT`, `RECIPIENTS_NO_EMAIL`, `RECIPIENTS_NO_LINKEDIN`, `MC_LINKEDIN_DAILY_LIMIT_CONNECTION`, `MC_LINKEDIN_DAILY_LIMIT`, `DAILY_CONTACT_TOO_LOW`, `LINKEDIN_DAILY_CONTACT_LIMIT`, `VERIFY_EMAIL_TOKEN_INVALID`, `VERIFY_EMAIL_TOKEN_EXPIRED`, `FREE_CREDITS_DOMAIN_ALREADY_CLAIMED`, `FREE_CREDITS_ALREADY_GRANTED`, `FREE_CREDITS_NOT_ELIGIBLE`, `RATE_LIMITED`, `STATS_COMPUTING`, `WARMUP_PROVIDER_UNSUPPORTED`, `ACCOUNT_DISCONNECTED` | no | Stable machine readable error code. Present only on catalogued errors (mainly the campaign start checks), on validation errors (`INVALID_REQUEST`) and on some LinkedIn provider errors (`ACCOUNT_DISCONNECTED`, `RATE_LIMITED`). Most other errors have no code: rely on the HTTP status and on `error`. Codes are never renamed once deployed. |
| `params` | object | no | Values interpolated in the message, for example `{"max": 30000}`. |

```json
{
  "error": "string",
  "code": "INVALID_REQUEST",
  "params": {}
}
```

### 401: Missing, malformed, unknown or disabled API key. The key must be exactly 48 characters, otherwise it is treated as a session token and rejected the same way.

| Field | Type | Required | Description |
|---|---|---|---|
| `error` | string or object | yes | Human readable message. A few legacy routes return an empty object `{}` here instead of a string. |
| `code` | enum: `INVALID_REQUEST`, `INVALID_PARAMETERS`, `SUBSCRIPTION_REQUIRED`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_ALREADY_RUNNING`, `SCHEDULE_MISSING`, `STEPS_MISSING`, `NO_RECIPIENTS`, `TOO_MANY_RECIPIENTS`, `PLANNED_START_INVALID`, `PLANNED_START_IN_PAST`, `EMAIL_PROVIDER_MISSING`, `EMAIL_PROVIDER_DISCONNECTED`, `EMAIL_PROVIDER_EXPIRED`, `ONE_EMAIL_PROVIDER_EXPIRED`, `EMAIL_ADDRESS_DISCONNECTED`, `TRACKING_DOMAINS_REQUIRED`, `LINKEDIN_ACCOUNT_MISSING`, `LINKEDIN_COOKIE_EXPIRED`, `LINKEDIN_SLOT_EXPIRED`, `WHATSAPP_ACCOUNT_EXPIRED`, `WHATSAPP_SLOT_EXPIRED`, `NO_IDENTITY_SET`, `IDENTITY_FOREIGN_ACCOUNT`, `IDENTITY_EMAIL_PROVIDER_MISSING`, `LINKEDIN_ACCOUNT_AMBIGUOUS`, `WHATSAPP_ACCOUNT_AMBIGUOUS`, `LINKEDIN_AUDIO_MISSING`, `EMAIL_STEP_NO_ACTIVE_VERSION`, `EMAIL_VERSION_NO_CONTENT`, `EMAIL_VERSION_NO_SUBJECT`, `RECIPIENTS_NO_EMAIL`, `RECIPIENTS_NO_LINKEDIN`, `MC_LINKEDIN_DAILY_LIMIT_CONNECTION`, `MC_LINKEDIN_DAILY_LIMIT`, `DAILY_CONTACT_TOO_LOW`, `LINKEDIN_DAILY_CONTACT_LIMIT`, `VERIFY_EMAIL_TOKEN_INVALID`, `VERIFY_EMAIL_TOKEN_EXPIRED`, `FREE_CREDITS_DOMAIN_ALREADY_CLAIMED`, `FREE_CREDITS_ALREADY_GRANTED`, `FREE_CREDITS_NOT_ELIGIBLE`, `RATE_LIMITED`, `STATS_COMPUTING`, `WARMUP_PROVIDER_UNSUPPORTED`, `ACCOUNT_DISCONNECTED` | no | Stable machine readable error code. Present only on catalogued errors (mainly the campaign start checks), on validation errors (`INVALID_REQUEST`) and on some LinkedIn provider errors (`ACCOUNT_DISCONNECTED`, `RATE_LIMITED`). Most other errors have no code: rely on the HTTP status and on `error`. Codes are never renamed once deployed. |
| `params` | object | no | Values interpolated in the message, for example `{"max": 30000}`. |

```json
{
  "error": "Authentication required"
}
```

### 500: Unexpected error. IMPORTANT: this is also what you receive today when the per minute quota of your account is exceeded (the quota error is not mapped to 429 for API keys). No `Retry-After` header is sent. Wait until the next minute window before retrying.

| Field | Type | Required | Description |
|---|---|---|---|
| `error` | string or object | yes | Human readable message. A few legacy routes return an empty object `{}` here instead of a string. |
| `code` | enum: `INVALID_REQUEST`, `INVALID_PARAMETERS`, `SUBSCRIPTION_REQUIRED`, `CAMPAIGN_NOT_FOUND`, `CAMPAIGN_ALREADY_RUNNING`, `SCHEDULE_MISSING`, `STEPS_MISSING`, `NO_RECIPIENTS`, `TOO_MANY_RECIPIENTS`, `PLANNED_START_INVALID`, `PLANNED_START_IN_PAST`, `EMAIL_PROVIDER_MISSING`, `EMAIL_PROVIDER_DISCONNECTED`, `EMAIL_PROVIDER_EXPIRED`, `ONE_EMAIL_PROVIDER_EXPIRED`, `EMAIL_ADDRESS_DISCONNECTED`, `TRACKING_DOMAINS_REQUIRED`, `LINKEDIN_ACCOUNT_MISSING`, `LINKEDIN_COOKIE_EXPIRED`, `LINKEDIN_SLOT_EXPIRED`, `WHATSAPP_ACCOUNT_EXPIRED`, `WHATSAPP_SLOT_EXPIRED`, `NO_IDENTITY_SET`, `IDENTITY_FOREIGN_ACCOUNT`, `IDENTITY_EMAIL_PROVIDER_MISSING`, `LINKEDIN_ACCOUNT_AMBIGUOUS`, `WHATSAPP_ACCOUNT_AMBIGUOUS`, `LINKEDIN_AUDIO_MISSING`, `EMAIL_STEP_NO_ACTIVE_VERSION`, `EMAIL_VERSION_NO_CONTENT`, `EMAIL_VERSION_NO_SUBJECT`, `RECIPIENTS_NO_EMAIL`, `RECIPIENTS_NO_LINKEDIN`, `MC_LINKEDIN_DAILY_LIMIT_CONNECTION`, `MC_LINKEDIN_DAILY_LIMIT`, `DAILY_CONTACT_TOO_LOW`, `LINKEDIN_DAILY_CONTACT_LIMIT`, `VERIFY_EMAIL_TOKEN_INVALID`, `VERIFY_EMAIL_TOKEN_EXPIRED`, `FREE_CREDITS_DOMAIN_ALREADY_CLAIMED`, `FREE_CREDITS_ALREADY_GRANTED`, `FREE_CREDITS_NOT_ELIGIBLE`, `RATE_LIMITED`, `STATS_COMPUTING`, `WARMUP_PROVIDER_UNSUPPORTED`, `ACCOUNT_DISCONNECTED` | no | Stable machine readable error code. Present only on catalogued errors (mainly the campaign start checks), on validation errors (`INVALID_REQUEST`) and on some LinkedIn provider errors (`ACCOUNT_DISCONNECTED`, `RATE_LIMITED`). Most other errors have no code: rely on the HTTP status and on `error`. Codes are never renamed once deployed. |
| `params` | object | no | Values interpolated in the message, for example `{"max": 30000}`. |

```json
{
  "error": "Internal server error"
}
```
