> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.heyreach.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.heyreach.io/_mcp/server.

# CreateV2

POST https://api//api/public/campaign/CreateV2
Content-Type: application/json

## Creates a campaign in DRAFT status, with an optional sequence that may include voice notes.

The v2 version of **Create**. It takes the same body, except that `sequence` is a **PublicSequenceNodeDtoV2**: a `MESSAGE` node carries `messageVersions` (text and voice versions) instead of `messages`. Use it instead of **Create** whenever the new campaign has a voice step. The node, `MESSAGE` payload and message version fields, and the voice rules that return 400, are described on **UpdateSequenceV2** and apply here unchanged - except that a DYNAMIC script with fewer than 3 spoken words is always refused here, since nothing is stored yet.

After the campaign is configured, call **StartCampaign** (`POST /api/public/campaign/StartCampaign?campaignId=`) to activate it. A campaign cannot be started without a sequence; if you omit `sequence`, add one with **UpdateSequenceV2** first. When the sequence has a DYNAMIC voice version, every sender needs a voice assigned (**SetSenderVoice**), or **StartCampaign** returns 400 `SENDER_VOICE_MISSING`.

### Body Parameters

* **name** *(string, required)*: The campaign name. 1-50 characters.

* **linkedInUserListId** *(long, required)*: ID of the lead list. Must exist and be of type USER\_LIST.

* **linkedInAccountIds** *(int\[], required)*: Sender LinkedIn account IDs. 1-100 items. All accounts must exist and be connected.

* **excludeContactedFromOtherCampaigns** *(bool, optional)*: Skip leads already in any other campaign. Default `false`.

* **excludeHasOtherAccConversations** *(bool, optional)*: Skip leads that already have conversations with other sender accounts. Default `false`.

* **excludeContactedFromSenderInOtherCampaign** *(bool, optional)*: Skip leads the sender accounts have already contacted in other campaigns. Default `false`.

* **excludeListId** *(long, optional)*: A separate list of leads to always exclude. Must not equal `linkedInUserListId`.

* **excludeFromLeadBlacklist** *(bool, optional)*: Skip leads on the workspace's lead blacklist. Default `true`; send `false` to opt out.

* **excludeFromCompanyBlacklist** *(bool, optional)*: Skip leads whose company is on the workspace's company blacklist. Default `true`; send `false` to opt out.

* **schedule** *(CampaignScheduleApiDto, optional)*: When the campaign runs. See **UpdateSchedule** for the object shape. If omitted, defaults to Mon-Fri 09:00-17:00 UTC.

* **sequence** *(PublicSequenceNodeDtoV2, optional)*: The automation workflow. The START node is implicit - do not include it - and every path must end with an `END` node.

### Errors

Every `{ "errorMessage": "..." }` error body carries an `errorCode`; branch on the code, not on the message. Request-validation 400s (for example a missing `name`, or an empty `linkedInAccountIds`) use the standard problem-details shape instead.

* **400** `LEAD_LIST_NOT_FOUND`, `LEAD_LIST_TYPE_INVALID`, `EXCLUDE_LIST_SAME_AS_LEAD_LIST`, `LINKEDIN_ACCOUNT_NOT_FOUND`, `LINKEDIN_ACCOUNT_AUTH_INVALID` (a sender is disconnected), `CAMPAIGN_SCHEDULE_INVALID`, `CAMPAIGN_TIMEZONE_INVALID`, or one of the sequence codes listed on **UpdateSequenceV2** other than `CAMPAIGN_STATUS_NOT_EDITABLE` and `SEQUENCE_UPDATE_INCONSISTENT`.
* **403** `NO_ACTIVE_SUBSCRIPTION`: the workspace has no active subscription.
* **404** `CAMPAIGN_API_NOT_ENABLED` (the campaign API is not enabled for your account), and `INTEGRATION_RESOURCE_NOT_FOUND` (the Instantly, Smartlead or EmailBison campaign or list of a `SEND_LEAD_TO_*` node was not found).

Reference: https://docs.heyreach.io/hey-reach-api/campaigns/create-v-2

## Request

### Headers

- `X-API-KEY` (string, optional) — API key header using this scheme. Example: "X-API-KEY: \{API\_KEY}"

### Body (application/json)

This endpoint expects an object.

- `name` (string, required) — The campaign name.
- `linkedInUserListId` (long, required) — ID of the lead list. Must exist and be of type USER_LIST.
- `linkedInAccountIds` (list of integer, required) — Sender LinkedIn account IDs. All accounts must exist and be connected.
- `excludeContactedFromOtherCampaigns` (boolean, optional, default: false)
- `excludeHasOtherAccConversations` (boolean, optional, default: false)
- `excludeContactedFromSenderInOtherCampaign` (boolean, optional, default: false)
- `excludeListId` (long, optional, nullable) — A separate list of leads to always exclude. Must not equal `linkedInUserListId`.
- `excludeFromLeadBlacklist` (boolean, optional, default: true)
- `excludeFromCompanyBlacklist` (boolean, optional, default: true)
- `schedule` (map from string to any, optional) — When the campaign runs. See **UpdateSchedule** for every field. If omitted, defaults to Mon-Fri 09:00-17:00 UTC.
- `sequence` (PublicSequenceNodeDtoV2, optional) — One node of a campaign sequence on the v2 endpoints (**GetCampaignSequenceV2**, **UpdateSequenceV2**, **CreateV2**). Nodes form a tree through `conditionalNode` and `unconditionalNode`. The START node is implicit - the root is your first action node - and every path ends with an `END` node.

## Response

### 200

OK

- `campaignId` (integer, required)

## Errors

### 400 Create V2campaigns Request Bad Request Error

Bad Request. See **Errors** above for what each `errorCode` means.

- `errorMessage` (string, required)
- `errorCode` (enum, optional)
  - Allowed values: `PAGING_OUT_OF_RANGE`

### 403 Create V2campaigns Request Forbidden Error

Forbidden. `NO_ACTIVE_SUBSCRIPTION`: the workspace has no active subscription.

- `errorMessage` (string, required)
- `errorCode` (enum, optional)
  - Allowed values: `NO_ACTIVE_SUBSCRIPTION`, `VOICE_NOTES_NOT_ENABLED`, `ELEVENLABS_NOT_ENABLED`

### 404 Create V2campaigns Request Not Found Error

Not Found. `CAMPAIGN_API_NOT_ENABLED`: the campaign API is not enabled for your account. `INTEGRATION_RESOURCE_NOT_FOUND`: the Instantly, Smartlead or EmailBison campaign or list of a `SEND_LEAD_TO_*` node was not found.

- `errorMessage` (string, required)
- `errorCode` (enum, optional)
  - Allowed values: `ELEVENLABS_VOICE_NOT_FOUND`, `ELEVENLABS_VOICE_ACCESS_DENIED`

## Types

### PublicSequenceNodeDtoV2

One node of a campaign sequence on the v2 endpoints (**GetCampaignSequenceV2**, **UpdateSequenceV2**, **CreateV2**). Nodes form a tree through `conditionalNode` and `unconditionalNode`. The START node is implicit - the root is your first action node - and every path ends with an `END` node.

- `nodeType` (enum, required) — The action or check this node performs. Required on every node.
  - Allowed values: `CONNECTION_REQUEST`, `MESSAGE`, `INMAIL`, `VIEW_PROFILE`, `FOLLOW`, `LIKE_POST`, `END`, `CHECK_IS_CONNECTION`, `CHECK_IS_OPEN_PROFILE`, `SEND_LEAD_TO_INSTANTLY`, `FIND_EMAIL`, `SEND_LEAD_TO_SMARTLEAD`, `SEND_LEAD_TO_BISON`
- `actionDelay` (integer, optional, default: 0) — The wait before this node runs, in `actionDelayUnit`. A node after an action step needs at least 3 hours - see **UpdateSequenceV2**.
- `actionDelayUnit` (enum, optional, default: HOUR) — Unit of `actionDelay`.
  - Allowed values: `HOUR`, `DAY`
- `payload` (PublicSequenceNodeDtoV2Payload, optional) — Node-specific configuration. A `MESSAGE` node uses the v2 message payload; every other node type uses its v1 payload (see **UpdateSequence**). `VIEW_PROFILE`, `FOLLOW`, `FIND_EMAIL`, `CHECK_IS_CONNECTION`, `CHECK_IS_OPEN_PROFILE` and `END` have no payload.
- `conditionalNode` (PublicSequenceNodeDtoV2, optional) — The next node when the branching condition is `true`. On `MESSAGE` and `INMAIL`, the `END` node taken when the lead replies.
- `unconditionalNode` (PublicSequenceNodeDtoV2, optional) — The next node to follow unconditionally, or when a branching condition is `false`. Required for every node except `END`.
- `externalReference` (string, optional, nullable) — Your own tracking ID for this step. Left out of responses when not set.

### PublicSequenceNodeDtoV2Payload

Node-specific configuration. A `MESSAGE` node uses the v2 message payload; every other node type uses its v1 payload (see **UpdateSequence**). `VIEW_PROFILE`, `FOLLOW`, `FIND_EMAIL`, `CHECK_IS_CONNECTION`, `CHECK_IS_OPEN_PROFILE` and `END` have no payload.

- Public Message Payload V2: PublicMessagePayloadV2
- map from string to any

### PublicMessagePayloadV2

The payload of a `MESSAGE` node on the v2 endpoints.

- `messageVersions` (list of PublicMessageVersion, required) — One entry per version of the step; one version is picked at random for each lead. At least one is required. On write the list replaces what is stored: versions you leave out are deleted.
- `fallbackMessage` (string, optional, nullable) — Plain text sent instead of a version whose text uses a variable the lead has no value for, and instead of a voice version whose voice note cannot be produced. Required when any version is VOICE. May use only `{FIRST_NAME}`, `{LAST_NAME}`, `{MY_FIRST_NAME}` and `{MY_LAST_NAME}`. Max 8000 characters.

### PublicMessageVersion

One version of a `MESSAGE` step: a text message, an AI voice note (VOICE + DYNAMIC) or an uploaded recording (VOICE + STATIC).

- `message` (string, optional, nullable) — TEXT: the message text, max 8000 characters. DYNAMIC: the spoken script, max 800 characters and at least 3 spoken words. STATIC: must be empty.
- `type` (enum, optional, default: TEXT)
  - Allowed values: `TEXT`, `VOICE`
- `voiceMode` (enum, optional, default: DYNAMIC) — Used only when `type` is `VOICE`.
  - Allowed values: `DYNAMIC`, `STATIC`
- `messageId` (string, optional, nullable) — The version's id. Send back the id GetCampaignSequenceV2 returned when you edit a sequence; leave it out only for a new version. Unique within a step; `FALLBACK`, `MANUAL` and `unknown` are reserved.
- `staticClipId` (long, optional, nullable) — STATIC only, and required there: the `clipId` returned by **UploadStaticClip**. Refused on TEXT and DYNAMIC versions.
- `durationMs` (integer, optional, nullable) — Length of the version's recording in milliseconds. Ignored on write.
- `transcript` (string, optional, nullable) — Transcript of the version's recording. Null is normal and never blocks sending. Ignored on write.
- `staticClipTranscript` (string, optional, nullable) — Read-only on a STATIC version: always the recording's own transcript, and a value you send is ignored. Must be empty on TEXT and DYNAMIC versions.
- `importedTranscript` (string, optional, nullable) — Read-only on a STATIC version: the script a shared step arrived with, shown only while it has no recording, and ignored on write. Must be empty on TEXT and DYNAMIC versions.
- `needsRecording` (boolean, optional) — True on a STATIC version that has no recording. Give such a version a `staticClipId` before you send it back, or the write is rejected with 400. The value you send is ignored.

## Examples

**Request**

```json
{
  "name": "My New Voice Campaign",
  "linkedInUserListId": 123456,
  "linkedInAccountIds": [
    11111,
    22222
  ],
  "schedule": {
    "dailyEndTime": "17:00:00",
    "dailyStartTime": "09:00:00",
    "enabledFriday": true,
    "enabledMonday": true,
    "enabledSaturday": false,
    "enabledSunday": false,
    "enabledThursday": true,
    "enabledTuesday": true,
    "enabledWednesday": true,
    "endDate": null,
    "startDate": null,
    "timeZoneId": "Europe/London"
  },
  "sequence": {
    "nodeType": "CONNECTION_REQUEST",
    "actionDelay": 0,
    "actionDelayUnit": "DAY",
    "payload": {
      "fallbackMessage": "Hi there, I'd love to connect!",
      "messageVersions": [
        {}
      ]
    }
  }
}
```

**Response**

```json
{
  "campaignId": 12345
}
```

**SDK Code**

```python
import requests

url = "https://api//api/public/campaign/CreateV2"

payload = {
    "name": "My New Voice Campaign",
    "linkedInUserListId": 123456,
    "linkedInAccountIds": [11111, 22222],
    "schedule": {
        "dailyEndTime": "17:00:00",
        "dailyStartTime": "09:00:00",
        "enabledFriday": True,
        "enabledMonday": True,
        "enabledSaturday": False,
        "enabledSunday": False,
        "enabledThursday": True,
        "enabledTuesday": True,
        "enabledWednesday": True,
        "endDate": None,
        "startDate": None,
        "timeZoneId": "Europe/London"
    },
    "sequence": {
        "nodeType": "CONNECTION_REQUEST",
        "actionDelay": 0,
        "actionDelayUnit": "DAY",
        "payload": {
            "fallbackMessage": "Hi there, I'd love to connect!",
            "messageVersions": [{}]
        }
    }
}
headers = {
    "X-API-KEY": "{{HR_API_KEY}}",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript
const url = 'https://api//api/public/campaign/CreateV2';
const options = {
  method: 'POST',
  headers: {'X-API-KEY': '{{HR_API_KEY}}', 'Content-Type': 'application/json'},
  body: '{"name":"My New Voice Campaign","linkedInUserListId":123456,"linkedInAccountIds":[11111,22222],"schedule":{"dailyEndTime":"17:00:00","dailyStartTime":"09:00:00","enabledFriday":true,"enabledMonday":true,"enabledSaturday":false,"enabledSunday":false,"enabledThursday":true,"enabledTuesday":true,"enabledWednesday":true,"endDate":null,"startDate":null,"timeZoneId":"Europe/London"},"sequence":{"nodeType":"CONNECTION_REQUEST","actionDelay":0,"actionDelayUnit":"DAY","payload":{"fallbackMessage":"Hi there, I\'d love to connect!","messageVersions":[{}]}}}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api//api/public/campaign/CreateV2"

	payload := strings.NewReader("{\n  \"name\": \"My New Voice Campaign\",\n  \"linkedInUserListId\": 123456,\n  \"linkedInAccountIds\": [\n    11111,\n    22222\n  ],\n  \"schedule\": {\n    \"dailyEndTime\": \"17:00:00\",\n    \"dailyStartTime\": \"09:00:00\",\n    \"enabledFriday\": true,\n    \"enabledMonday\": true,\n    \"enabledSaturday\": false,\n    \"enabledSunday\": false,\n    \"enabledThursday\": true,\n    \"enabledTuesday\": true,\n    \"enabledWednesday\": true,\n    \"endDate\": null,\n    \"startDate\": null,\n    \"timeZoneId\": \"Europe/London\"\n  },\n  \"sequence\": {\n    \"nodeType\": \"CONNECTION_REQUEST\",\n    \"actionDelay\": 0,\n    \"actionDelayUnit\": \"DAY\",\n    \"payload\": {\n      \"fallbackMessage\": \"Hi there, I'd love to connect!\",\n      \"messageVersions\": [\n        {}\n      ]\n    }\n  }\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("X-API-KEY", "{{HR_API_KEY}}")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby
require 'uri'
require 'net/http'

url = URI("https://api//api/public/campaign/CreateV2")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["X-API-KEY"] = '{{HR_API_KEY}}'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"name\": \"My New Voice Campaign\",\n  \"linkedInUserListId\": 123456,\n  \"linkedInAccountIds\": [\n    11111,\n    22222\n  ],\n  \"schedule\": {\n    \"dailyEndTime\": \"17:00:00\",\n    \"dailyStartTime\": \"09:00:00\",\n    \"enabledFriday\": true,\n    \"enabledMonday\": true,\n    \"enabledSaturday\": false,\n    \"enabledSunday\": false,\n    \"enabledThursday\": true,\n    \"enabledTuesday\": true,\n    \"enabledWednesday\": true,\n    \"endDate\": null,\n    \"startDate\": null,\n    \"timeZoneId\": \"Europe/London\"\n  },\n  \"sequence\": {\n    \"nodeType\": \"CONNECTION_REQUEST\",\n    \"actionDelay\": 0,\n    \"actionDelayUnit\": \"DAY\",\n    \"payload\": {\n      \"fallbackMessage\": \"Hi there, I'd love to connect!\",\n      \"messageVersions\": [\n        {}\n      ]\n    }\n  }\n}"

response = http.request(request)
puts response.read_body
```

```java
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api//api/public/campaign/CreateV2")
  .header("X-API-KEY", "{{HR_API_KEY}}")
  .header("Content-Type", "application/json")
  .body("{\n  \"name\": \"My New Voice Campaign\",\n  \"linkedInUserListId\": 123456,\n  \"linkedInAccountIds\": [\n    11111,\n    22222\n  ],\n  \"schedule\": {\n    \"dailyEndTime\": \"17:00:00\",\n    \"dailyStartTime\": \"09:00:00\",\n    \"enabledFriday\": true,\n    \"enabledMonday\": true,\n    \"enabledSaturday\": false,\n    \"enabledSunday\": false,\n    \"enabledThursday\": true,\n    \"enabledTuesday\": true,\n    \"enabledWednesday\": true,\n    \"endDate\": null,\n    \"startDate\": null,\n    \"timeZoneId\": \"Europe/London\"\n  },\n  \"sequence\": {\n    \"nodeType\": \"CONNECTION_REQUEST\",\n    \"actionDelay\": 0,\n    \"actionDelayUnit\": \"DAY\",\n    \"payload\": {\n      \"fallbackMessage\": \"Hi there, I'd love to connect!\",\n      \"messageVersions\": [\n        {}\n      ]\n    }\n  }\n}")
  .asString();
```

```php
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api//api/public/campaign/CreateV2', [
  'body' => '{
  "name": "My New Voice Campaign",
  "linkedInUserListId": 123456,
  "linkedInAccountIds": [
    11111,
    22222
  ],
  "schedule": {
    "dailyEndTime": "17:00:00",
    "dailyStartTime": "09:00:00",
    "enabledFriday": true,
    "enabledMonday": true,
    "enabledSaturday": false,
    "enabledSunday": false,
    "enabledThursday": true,
    "enabledTuesday": true,
    "enabledWednesday": true,
    "endDate": null,
    "startDate": null,
    "timeZoneId": "Europe/London"
  },
  "sequence": {
    "nodeType": "CONNECTION_REQUEST",
    "actionDelay": 0,
    "actionDelayUnit": "DAY",
    "payload": {
      "fallbackMessage": "Hi there, I\'d love to connect!",
      "messageVersions": [
        {}
      ]
    }
  }
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'X-API-KEY' => '{{HR_API_KEY}}',
  ],
]);

echo $response->getBody();
```

```csharp
using RestSharp;

var client = new RestClient("https://api//api/public/campaign/CreateV2");
var request = new RestRequest(Method.POST);
request.AddHeader("X-API-KEY", "{{HR_API_KEY}}");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"name\": \"My New Voice Campaign\",\n  \"linkedInUserListId\": 123456,\n  \"linkedInAccountIds\": [\n    11111,\n    22222\n  ],\n  \"schedule\": {\n    \"dailyEndTime\": \"17:00:00\",\n    \"dailyStartTime\": \"09:00:00\",\n    \"enabledFriday\": true,\n    \"enabledMonday\": true,\n    \"enabledSaturday\": false,\n    \"enabledSunday\": false,\n    \"enabledThursday\": true,\n    \"enabledTuesday\": true,\n    \"enabledWednesday\": true,\n    \"endDate\": null,\n    \"startDate\": null,\n    \"timeZoneId\": \"Europe/London\"\n  },\n  \"sequence\": {\n    \"nodeType\": \"CONNECTION_REQUEST\",\n    \"actionDelay\": 0,\n    \"actionDelayUnit\": \"DAY\",\n    \"payload\": {\n      \"fallbackMessage\": \"Hi there, I'd love to connect!\",\n      \"messageVersions\": [\n        {}\n      ]\n    }\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift
import Foundation

let headers = [
  "X-API-KEY": "{{HR_API_KEY}}",
  "Content-Type": "application/json"
]
let parameters = [
  "name": "My New Voice Campaign",
  "linkedInUserListId": 123456,
  "linkedInAccountIds": [11111, 22222],
  "schedule": [
    "dailyEndTime": "17:00:00",
    "dailyStartTime": "09:00:00",
    "enabledFriday": true,
    "enabledMonday": true,
    "enabledSaturday": false,
    "enabledSunday": false,
    "enabledThursday": true,
    "enabledTuesday": true,
    "enabledWednesday": true,
    "endDate": ,
    "startDate": ,
    "timeZoneId": "Europe/London"
  ],
  "sequence": [
    "nodeType": "CONNECTION_REQUEST",
    "actionDelay": 0,
    "actionDelayUnit": "DAY",
    "payload": [
      "fallbackMessage": "Hi there, I'd love to connect!",
      "messageVersions": [[]]
    ]
  ]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api//api/public/campaign/CreateV2")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```