Create campaign draft
Create an email, LinkedIn, or multichannel campaign without providing all the details required to run it. The campaign is created in DRAFT status and can include a sequence of steps even if some configuration required to run them is still missing, such as mailboxes, LinkedIn accounts, or message content. Before you can run the campaign you must complete any missing configuration and content.
- To create a campaign that has the configuration and content needed to run, use create campaign
- To send one email to one prospect and start the campaign automatically, use create and run a simple campaign
Request
Endpoint
POST https://api.woodpecker.co/rest/v2/campaigns/create-draft
Headers
x-api-key: {YOUR_API_KEY}
Content-Type: application/json
For details on how to authenticate your requests, see the authentication guide.
Body
The request body uses the same campaign structure as create campaign, but fewer fields are required. A draft must still include a START step followed by at least one EMAIL or LINKEDIN step. Email steps require only delivery_time, while LinkedIn steps require body.action_type.
- Minimal email draft
- Email and LinkedIn draft
Create an email campaign without a mailbox, campaign settings, subject, or message. Woodpecker fills in the omitted settings and creates an empty email version.
{
"name": "Campaign draft",
"steps": {
"type": "START",
"followup": {
"type": "EMAIL",
"delivery_time": {
"MONDAY": [{ "from": "08:00", "to": "18:00" }]
}
}
}
}
Create a two-step campaign with settings and message content, but without a selected mailbox or LinkedIn account. Add the accounts before running it.
{
"name": "Outreach draft",
"email_account_ids": [],
"settings": {
"timezone": "Europe/Warsaw",
"daily_enroll": 25
},
"steps": {
"type": "START",
"followup": {
"type": "EMAIL",
"delivery_time": {
"MONDAY": [{ "from": "09:00", "to": "17:00" }],
"TUESDAY": [{ "from": "09:00", "to": "17:00" }],
"WEDNESDAY": [{ "from": "09:00", "to": "17:00" }],
"THURSDAY": [{ "from": "09:00", "to": "17:00" }],
"FRIDAY": [{ "from": "09:00", "to": "17:00" }]
},
"body": {
"versions": [
{
"subject": "An idea for your team",
"message": "<div>Hi there, I'd like to share an idea for Pied Piper.</div>",
"signature": "NO_SIGNATURE",
"track_opens": false
}
]
},
"followup": {
"type": "LINKEDIN",
"followup_after": { "range": "DAY", "value": 2 },
"body": {
"action_type": "CONNECTION_REQUEST",
"versions": [
{ "message": "Hi, I'd like to connect and share an idea for Pied Piper" }
]
}
}
}
}
}
Body schema
Notes:
- If an email step does not include body or versions, Woodpecker creates one email version with an empty subject and message. Missing or null values in a supplied version are also treated as empty. Empty subjects are returned as null.
- Fields that are provided are still validated. This includes mailbox IDs, campaign settings, delays, and delivery windows.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Campaign name; uses the standard generated campaign name when omitted |
email_account_ids | array[integer] | No | SMTP mailbox IDs; omit or use [] when none are connected yet. Supplied IDs must refer to usable mailboxes; see get mailboxes |
settings | object | No | Campaign settings; omitted values use defaults described below |
└─ timezone | string | No | Campaign timezone; defaults to the requesting user's timezone |
└─ daily_enroll | integer | No | Daily enrollment limit; defaults to 50 |
steps | object | Yes | Root START step containing the campaign's first action step in followup |
└─ type | string | Yes | Must be START |
└─ followup | object | Yes | First EMAIL or LINKEDIN step |
└─ type | string | Yes | EMAIL or LINKEDIN |
└─ delivery_time | object | For EMAIL | Sending windows, by weekday; see the delivery time schema |
└─ body | object | For LINKEDIN | Contains the required action_type; see the LinkedIn step schema |
└─ body.versions | array[object] | No | Email or LinkedIn message versions; when omitted or empty, one empty version is created |
└─ body.linkedin_account_id | integer/null | No | LinkedIn account for the step; it can be set later. See get LinkedIn accounts |
└─ followup_after | object | No | Delay before this step; defaults to one day |
└─ followup | object/null | No | Next email or LinkedIn step; omit for the final step |
Request samples
The samples below use the minimal email draft shown above.
Create an email campaign draft
- cURL
- Python
- Java
- Node.js
- PHP
curl --request POST \
--url "https://api.woodpecker.co/rest/v2/campaigns/create-draft" \
--header "x-api-key: {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--data '{
"name": "Campaign draft",
"steps": {
"type": "START",
"followup": {
"type": "EMAIL",
"delivery_time": {
"MONDAY": [{ "from": "08:00", "to": "18:00" }]
}
}
}
}'
import requests
def create_campaign_draft():
url = "https://api.woodpecker.co/rest/v2/campaigns/create-draft"
headers = {
"x-api-key": "{YOUR_API_KEY}",
"Content-Type": "application/json"
}
payload = {
"name": "Campaign draft",
"steps": {
"type": "START",
"followup": {
"type": "EMAIL",
"delivery_time": {
"MONDAY": [{"from": "08:00", "to": "18:00"}]
}
}
}
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 201:
return response.json()
raise Exception(f"POST request failed: {response.status_code}, {response.text}")
if __name__ == "__main__":
try:
data = create_campaign_draft()
print("POST response:", data)
except Exception as e:
print("Error:", e)
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class WoodpeckerApiClient {
public static void main(String[] args) {
try {
String url = "https://api.woodpecker.co/rest/v2/campaigns/create-draft";
String jsonData = """
{
"name": "Campaign draft",
"steps": {
"type": "START",
"followup": {
"type": "EMAIL",
"delivery_time": {
"MONDAY": [{ "from": "08:00", "to": "18:00" }]
}
}
}
}
""";
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("x-api-key", "{YOUR_API_KEY}")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonData))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 201) {
System.out.println("POST response: " + response.body());
} else {
throw new Exception("POST request failed: " + response.statusCode() + ", " + response.body());
}
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
}
const axios = require('axios');
async function createCampaignDraft() {
const url = 'https://api.woodpecker.co/rest/v2/campaigns/create-draft';
const headers = {
'x-api-key': '{YOUR_API_KEY}',
'Content-Type': 'application/json'
};
const data = {
name: 'Campaign draft',
steps: {
type: 'START',
followup: {
type: 'EMAIL',
delivery_time: {
MONDAY: [{ from: '08:00', to: '18:00' }]
}
}
}
};
try {
const response = await axios.post(url, data, { headers });
console.log('POST response:', response.data);
} catch (error) {
console.error('POST request failed:', error.response ? error.response.status : error.message);
}
}
createCampaignDraft();
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client([
'base_uri' => 'https://api.woodpecker.co/rest/v2/',
'headers' => [
'x-api-key' => getenv('WOODPECKER_API_KEY'),
'Content-Type' => 'application/json',
],
]);
try {
$response = $client->post('campaigns/create-draft', [
'json' => [
'name' => 'Campaign draft',
'steps' => [
'type' => 'START',
'followup' => [
'type' => 'EMAIL',
'delivery_time' => [
'MONDAY' => [['from' => '08:00', 'to' => '18:00']],
],
],
],
],
]);
echo $response->getStatusCode(), "\n";
echo $response->getBody(), "\n";
} catch (RequestException $e) {
echo "Error: ", $e->getMessage(), "\n";
if ($e->hasResponse()) {
echo $e->getResponse()->getBody(), "\n";
}
}
Response
Response examples
- 201
- 400
- 401
- 409
- 500
Campaign created. The returned body will be a full campaign payload, which includes any optional fields that were not included in the request. The campaign is created in DRAFT status.
Use the returned id with get campaign, update campaign, and run campaign. Running the draft checks the missing content and accounts and returns a validation error until the campaign is ready.
{
"id": 200001,
"name": "Campaign draft",
"status": "DRAFT",
"bounce_shield_autopaused_at": null,
"email_account_ids": [],
"settings": {
"timezone": "Europe/Warsaw",
"prospect_timezone": false,
"daily_enroll": 50,
"gdpr_unsubscribe": false,
"list_unsubscribe": false,
"open_disabled_list": [],
"auto_pause_prospect_from_domain_statuses": null,
"auto_pause_prospect_from_domain": false,
"catch_all_verification_mode": "BALANCED",
"count_followup_delay_in_working_days": false
},
"steps": {
"id": "8c7554ce-a50c-49f7-9129-2ef5a15f9d9c",
"type": "START",
"followup": {
"id": "5486ed61-206c-49dc-b394-fb2524cf163e",
"type": "EMAIL",
"followup_after": { "range": "DAY", "value": 1 },
"followup": null,
"delivery_time": {
"MONDAY": [{ "from": "08:00", "to": "18:00" }]
},
"body": {
"versions": [
{
"id": "a5436b139434744d605261506c5a996f14c4c0411503807579bbc0585d6b9907",
"version": "A",
"subject": null,
"message": "",
"signature": "NO_SIGNATURE",
"track_opens": false
}
]
}
}
}
}
Invalid request or malformed request syntax. Please review the request body.
{
"code": "INPUT_DATA_VALIDATION_FAILURE",
"message": "Input data validation failure",
"details": "steps must be provided"
}
Body schema
| Field | Type | Description |
|---|---|---|
code | string | INPUT_DATA_VALIDATION_FAILURE |
message | string | Error message |
details | string/null | Additional details when available |
An issue with authorization. Please review the authorization guide
{
"title": "Unauthorized",
"status": 401,
"detail": "Invalid api key",
"timestamp": "2025-03-05 17:57:00"
}
Body schema
| Field | Type | Description |
|---|---|---|
title | string | A short title describing the error |
status | integer | The HTTP status code |
detail | string | A detailed message explaining the error |
timestamp | string | The timestamp when the error occurred, YYYY-MM-DD HH:MM:SS UTC |
The payload body passes the validation but there is an issue with requested data, for example the assigned email has connection problems
{
"code": "VALIDATION_FAILURE",
"message": "Validation failure",
"details": "String"
}
Body schema
| Field | Type | Description |
|---|---|---|
code | string | VALIDATION_FAILURE |
message | string | Error message |
details | string/null | Additional details when available |
Unexpected error, please try again later.
{
"code": "UNKNOWN",
"message": "Unknown error during campaign call",
"details": null
}
Body schema
| Field | Type | Description |
|---|---|---|
code | string | Error code |
message | string | Error message |
details | string/null | Additional information |