# Replace email campaign steps

Source: https://docs.emelia.io/docs/campaigns/legacy/email/replace-steps

Guide: [Email campaigns](https://docs.emelia.io/docs/campaigns/legacy/email.md)

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

Replaces the full ordered list of steps (the sequence). The campaign must not be RUNNING or ARCHIVED. Steps after the first must have delay.amount > 0, and a step may have at most one disabled version. Changing the step count resets the cached stats. New integrations should use the multichannel /advanced campaign endpoints instead.

> **Deprecated.**

## Authentication

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

## Path parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `campaignId` | string | yes | Mongo ObjectId of the campaign. |

## Request body

Content type: `application/json`, required.

| Field | Type | Required | Description |
|---|---|---|---|
| `steps` | array of object | yes |  |
| `steps[].delay` | object | yes |  |
| `steps[].delay.amount` | integer | no | Must be > 0 for steps after the first. Range: 0 to 10000. |
| `steps[].delay.unit` | enum: `MINUTES`, `HOURS`, `DAYS` | yes |  |
| `steps[].versions` | array of object | yes | A/B versions. At most one may be disabled. |
| `steps[].versions[].subject` | string | no |  |
| `steps[].versions[].disabled` | boolean | no |  |
| `steps[].versions[].message` | string | no | HTML body. Variables use {{firstName}}, {{custom_field}}, {{UNSUBSCRIBE_LINK}}, {{SENDER}}, {{UNIQUE_ID}}. |
| `steps[].versions[].rawHtml` | boolean | no |  |
| `steps[].versions[].attachments` | array of object | no |  |
| `steps[].versions[].options` | object | no | Per-version overrides (per-day schedule, tracking, provider). Usually omitted; derived from the campaign schedule. |

```json
{
  "steps": [
    {
      "delay": {
        "amount": 0,
        "unit": "DAYS"
      },
      "versions": [
        {
          "subject": "Quick question, {{firstName}}",
          "message": "<p>Hi {{firstName}},</p>"
        }
      ]
    },
    {
      "delay": {
        "amount": 2,
        "unit": "DAYS"
      },
      "versions": [
        {
          "subject": "",
          "message": "<p>Following up.</p>"
        }
      ]
    }
  ]
}
```

## Example request

**cURL**

```bash
curl -X PATCH "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/steps" \
  -H "Authorization: $EMELIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "steps": [
    {
      "delay": {
        "amount": 0,
        "unit": "DAYS"
      },
      "versions": [
        {
          "subject": "Quick question, {{firstName}}",
          "message": "<p>Hi {{firstName}},</p>"
        }
      ]
    },
    {
      "delay": {
        "amount": 2,
        "unit": "DAYS"
      },
      "versions": [
        {
          "subject": "",
          "message": "<p>Following up.</p>"
        }
      ]
    }
  ]
}'
```

**JavaScript**

```js
const response = await fetch('https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/steps', {
  method: 'PATCH',
  headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "steps": [
      {
        "delay": {
          "amount": 0,
          "unit": "DAYS"
        },
        "versions": [
          {
            "subject": "Quick question, {{firstName}}",
            "message": "<p>Hi {{firstName}},</p>"
          }
        ]
      },
      {
        "delay": {
          "amount": 2,
          "unit": "DAYS"
        },
        "versions": [
          {
            "subject": "",
            "message": "<p>Following up.</p>"
          }
        ]
      }
    ]
  }),
});
console.log(await response.json());
```

**Python**

```python
import os
import requests

response = requests.patch(
    "https://api.emelia.io/emails/campaigns/65f1a2b3c4d5e6f73478765e/steps",
    headers={"Authorization": os.environ["EMELIA_API_KEY"]},
    json={
        "steps": [
            {
                "delay": {
                    "amount": 0,
                    "unit": "DAYS"
                },
                "versions": [
                    {
                        "subject": "Quick question, {{firstName}}",
                        "message": "<p>Hi {{firstName}},</p>"
                    }
                ]
            },
            {
                "delay": {
                    "amount": 2,
                    "unit": "DAYS"
                },
                "versions": [
                    {
                        "subject": "",
                        "message": "<p>Following up.</p>"
                    }
                ]
            }
        ]
    },
)
print(response.json())
```

## Responses

### 200: Updated campaign.

| Field | Type | Required | Description |
|---|---|---|---|
| `success` | enum: `true` | yes |  |
| `campaign` | object | yes | A full email campaign document (as returned by create/get/update). |
| `campaign._id` | string | no |  |
| `campaign.userId` | string | no |  |
| `campaign.name` | string | no |  |
| `campaign.status` | enum: `DRAFT`, `RUNNING`, `PAUSED`, `FINISHED`, `ARCHIVED` | no |  |
| `campaign.provider` | string or null | no |  |
| `campaign.useManyProviders` | boolean | no |  |
| `campaign.providersUsed` | array of string | no |  |
| `campaign.schedule` | object | no |  |
| `campaign.steps` | array of object | no |  |
| `campaign.recipients` | object | no |  |
| `campaign.zapierData` | array of object | no |  |
| `campaign.createdAt` | string (date-time) | no |  |
| `campaign.startAt` | string (date-time) | no |  |
| `campaign.estimatedEnd` | string (date-time) | no |  |
| `campaign.plannedStart` | string (date-time) | no |  |

```json
{
  "success": true,
  "campaign": {
    "_id": "6650f1a2b839d8000b13890b",
    "userId": "6650f1a2b839d800555fdeb3",
    "name": "Q4 outreach: SaaS founders",
    "status": "DRAFT",
    "provider": "6650f1a2b839d800d6193fc9",
    "useManyProviders": true,
    "providersUsed": [
      "6650f1a2b839d800c15749e7"
    ],
    "schedule": {
      "bcc": "",
      "dailyContact": 35,
      "dailyLimit": 100,
      "minInterval": 60,
      "maxInterval": 180,
      "blacklistUnsub": true,
      "trackLinks": true,
      "trackOpens": true,
      "timeZone": "Europe/Paris",
      "days": [
        1
      ],
      "start": "08:00",
      "end": "17:00",
      "eventToStopMails": []
    },
    "steps": [
      {
        "_id": "6650f1a2b839d8004d4acc0a",
        "delay": {
          "amount": 1,
          "unit": "DAYS"
        },
        "versions": [
          {
            "_id": "6650f1a2b839d800ef395699",
            "subject": "Quick question about Nexora",
            "message": "Hi Claire, thanks for your reply. Are you free for a call on Thursday?",
            "disabled": false,
            "rawHtml": true,
            "attachments": [],
            "options": {}
          }
        ]
      }
    ],
    "recipients": {
      "lists": [
        {}
      ],
      "contacts": [
        "6650f1a2b839d800cdc683a1"
      ],
      "processing": true
    },
    "zapierData": [
      {
        "webhookUrl": "https://hooks.yourcompany.com/emelia",
        "events": []
      }
    ],
    "createdAt": "2026-10-06T09:30:00.000Z",
    "startAt": "2026-10-06T09:30:00.000Z",
    "estimatedEnd": "2026-10-06T09:30:00.000Z",
    "plannedStart": "2026-10-06T09:30:00.000Z"
  }
}
```

### 400: Business or validation error. 400 by default; Joi validation errors carry code 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": {}
}
```
