Create campaign
Create an email campaign with a sequence of personalized follow-up messages and customizable delivery settings. Configure campaign-wide settings like timezone and daily limits, define email content with support for A/B testing, scheduled delivery windows, and time delays between steps.
Request
Endpoint
POST https://api.woodpecker.co/rest/v2/campaigns
Headers
x-api-key: {YOUR_API_KEY}
Content-type: application/json
For details on how to authenticate your requests, please see the authentication guide.
Body
The campaign payload includes several objects, which are described in detail below. In addition to the general campaign configuration, a campaign is made up of steps that define its structure and actions for each prospect. Every campaign must begin with a START
step, followed by 1 to 16 EMAIL
steps. Subsequent steps are linked using nested follow-up properties, forming a sequence of steps. Each step includes its own configuration and points to the next step, or null
if it is the final step
- Only required fields
- All available fields
{
"email_account_ids": [
112233
],
"settings": {
"timezone": "Europe/Warsaw",
"daily_enroll": 40
},
"steps": {
"type": "START",
"followup": {
"type": "EMAIL",
"delivery_time": {
"MONDAY": [
{
"from": "08:00",
"to": "17:00"
}
]
},
"body": {
"versions": [
{
"subject": "Hi {{FIRST_NAME | \"there\"}}. {{SPINTAX | \"Hi\" | \"Hello\" | \"Good morning\"}}",
"message": "<div>Hi {{FIRST_NAME | \"there\"}},</div><div><br /></div><div>This is an example cold email message. </div><div><br /></div><div>Best wishes, </div><div><a href=\"https://woodpecker.co\">Woodpecker</a> team</div><div><a href=\"{{UNSUBSCRIBE}}\">unsubscribe</a></div>",
"track_opens": true
}
]
}
}
}
}
{
"name": "Three step campaign with custom configuration",
"email_account_ids": [100001, 100002, 100003],
"settings": {
"timezone": "Europe/Warsaw",
"prospect_timezone": true,
"daily_enroll": 80,
"gdpr_unsubscribe": true,
"list_unsubscribe": true,
"open_disabled_list": ["google.com", "OTHER_PROVIDER"],
"catch_all_verification_mode": "MAXIMUM",
"auto_pause_prospect_from_domain": true
},
"steps": {
"type": "START",
"followup": {
"type": "EMAIL",
"delivery_time": {
"TUESDAY": [{ "from": "09:00", "to": "18:00" }],
"WEDNESDAY": [{ "from": "09:00", "to": "18:00" }],
"THURSDAY": [{ "from": "09:00", "to": "18:00" }]
},
"body": {
"versions": [
{
"subject": "Example subject line - version A",
"message": "<div>Hi {{FIRST_NAME | \"there\"}},</div><div><br /></div><div>This is an example cold email message. </div><div><br /></div><div>Best wishes, </div><div><a href=\"https://woodpecker.co\">Woodpecker</a> team</div><div><a href=\"{{UNSUBSCRIBE}}\">unsubscribe</a></div>",
"signature": "SENDER",
"track_opens": true
},
{
"subject": "Example subject line - version B",
"message": "<div>{{SPINTAX | \"Hi\" | \"Hello\" | \"Good morning\"}} {{FIRST_NAME}},</div><div><br /></div><div>Yet another example of a cold email message. </div><div><br /></div><div>All the best, </div><div><a href=\"https://woodpecker.co\">Woodpecker</a> team</div><div><a href=\"{{UNSUBSCRIBE}}\">unsubscribe</a></div>",
"signature": "SENDER",
"track_opens": false
}
]
},
"followup_after": { "range": "DAY", "value": 2 },
"followup": {
"type": "EMAIL",
"delivery_time": { "FRIDAY": [{ "from": "09:00", "to": "17:00" }] },
"body": {
"versions": [
{
"subject": null,
"message": "<div>First followup, version A, sender's signature, no open tracking, same subject line</div>",
"signature": "SENDER",
"track_opens": false
},
{
"subject": null,
"message": "<div>First followup, version B, no signature, no open tracking, same subject line</div>",
"signature": "NO_SIGNATURE",
"track_opens": false
},
{
"subject": null,
"message": "<div>First followup, version C, sender's signature, open tracking, same subject line</div>",
"signature": "SENDER",
"track_opens": true
}
]
},
"followup_after": { "range": "DAY", "value": 6 },
"followup": {
"type": "EMAIL",
"delivery_time": {
"WEDNESDAY": [
{
"from": "09:00",
"to": "11:00"
},
{
"from": "14:00",
"to": "16:00"
}
],
"THURSDAY": [
{
"from": "09:00",
"to": "11:00"
},
{
"from": "14:00",
"to": "16:00"
}
]
},
"body": {
"versions": [
{
"subject": "Subject linie 3A",
"message": "<div>Third followup, sender's signature, no open tracking, different subject line</div>",
"signature": "SENDER",
"track_opens": false
},
{
"subject": "Subject linie 3B",
"message": "<div>Third followup, sender's signature, no open tracking, different subject line</div>",
"signature": "SENDER",
"track_opens": false
}
]
},
"followup_after": { "range": "DAY", "value": 1 }
}
}
}
}
}
Body schema
This section provides the body schema for each object in the campaign payloads. For a more detailed overview, please refer to the campaign schema.
Campaign configuration object
The root level of the campaign payload. It provides general information about the campaign and campaign-wide settings.
Field | Type | Default | Required | Description |
---|---|---|---|---|
name | string | “My campaign #0” | NO | Name of the campaign |
email_account_ids | array[integer] | - | YES | List of email account SMTP IDs used in this campaign. Use /mailboxes endpoint to review them. Chosen mailboxes must be connected to Woodpecker without issues |
settings | object | - | YES | Campaign-level settings like timezone, sending limit, unsubscribe settings, etc |
└─timezone | string | - | YES | The default timezone of a campaign. It will be used when setting.prospect_timezone is disabled or when it is enabled but the prospect's timezone is not specified. List of accepted timezones |
└─prospect_timezone | boolean | false | NO | Whether to adjust sending times to prospect's timezone instead of the campaign timezone |
└─daily_enroll | integer | - | YES | Maximum number of opening emails that can be sent per day. The limit is shared between all mailboxes. The default maximum value is 500 |
└─gdpr_unsubscribe | boolean | false | NO | Whether the unsubscribe link should provide prospects with an option for GDPR-compliant data removal. This option will work only if the {{UNSUBSCRIBE}} snippet is included in your email or account signature |
└─list_unsubscribe | boolean | false | NO | Whether to include List-Unsubscribe header. This option will work only if the {{UNSUBSCRIBE}} snippet is included in your email or account signature |
└─open_disabled_list | array[string]/null | [] | NO | List of email service providers (recipient's ESP) for which open tracking is disabled. Available options: google.com , outlook.com , OTHER_PROVIDER |
└─auto_pause_prospect_from_domain | boolean/null | false | NO | Whether to automatically pause sending to prospects after a response from the same domain (free providers excluded) |
└─catch_all_verification_mode | string | BALANCED | NO | Catch-all email verification mode - how to approach contacting prospects using catch-all emails.
|
steps | object | - | YES | Campaign steps, including all emails and their delivery times, content, etc |
"catch_all_verification_mode": "MAXIMUM", |
Steps objects
This object defines a step of a campaign, including its content, versions, delivery times, and follow-ups. A START
step contains only of type
and followup
properties.
Field | Type | Default | Required | Description |
---|---|---|---|---|
type | string | - | YES | START is always the first step of a campaign; the remaining steps are EMAIL |
delivery_time | object | - | YES | Time intervals during which emails can be sent |
body | object | - | YES | Email content configuration including A/B test versions |
followup_after | object | 1 DAY | NO | Object that specifies the time delay before processing a prospect in the next step; if delivery_time allows it |
└─range | string | DAY | NO | Time unit - DAY , HOUR , MINUTE |
└─value | integer | 1 | NO | Value of the time unit |
followup | object/null | null (meaning no followup) | NO | Next step in the sequence. Consists of an EMAIL step object. Null indicates end of sequence. Followup object is required in the START step |
Delivery time object
The delivery_time
object defines the time intervals during which email messages can be sent. The timezone will follow the settings of the timezone
and prospect_timezone
of the campaign configuration.
Field | Type | Default | Required | Description |
---|---|---|---|---|
MONDAY ...SUNDAY | array[object] | - | YES | Array of time windows for each day. Maximum 2 windows per day, at least one day must be present. The valid keys are the days of the week: MONDAY , TUESDAY , WEDNESDAY , THURSDAY , FRIDAY , SATURDAY , SUNDAY |
└─[].from | string | - | YES | Start time in "HH:mm" format (24-hour) |
└─[].to | string | - | YES | End time in "HH:mm" format (24-hour) |
Body object
The body
and versions
objects define the email content, A/B test versions, open tracking, and signature settings. Each of these can be configured individually for each version, and at least one version must be present.
Field | Type | Default | Required | Description |
---|---|---|---|---|
body.versions | array[object] | - | YES | Array of email version objects and their definitions. At least one version is required. A/B versions will be assigned based on the order of the array |
└─[].subject | string/null | - | YES, for the first EMAIL step | Email subject line. This field is required if the version is part of the first EMAIL step in the sequence. It must be included in all step versions if multiple versions exist. If the step is not the first EMAIL step and the subject field is null, the message will be sent as a follow-up in the same thread. Supports snippets like {{FIRST_NAME}} and spintax |
└─[].message | string | - | YES | Email body content in HTML format. Supports snippets like {{FIRST_NAME}} and spintax |
└─[].signature | string | NO_SIGNATURE | NO | Whether to use the sender's email account signature. The available options are: SENDER or NO_SIGNATURE |
└─[].track_opens | boolean | - | YES | Whether to track email opens for this email version |
Request sample
Create campaign
- cURL
- Java
- Node.js
curl --request POST \
--url "https://api.woodpecker.co/rest/v2/campaigns" \
--header "x-api-key: {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--data '{
"email_account_ids": [112233],
"settings": {
"timezone": "Europe/Warsaw",
"daily_enroll": 40
},
"steps": {
"type": "START",
"followup": {
"type": "EMAIL",
"delivery_time": {
"MONDAY": [
{
"from": "08:00",
"to": "17:00"
}
]
},
"body": {
"versions": [
{
"subject": "Hi {{FIRST_NAME | \"there\"}}. {{SPINTAX | \"Hi\" | \"Hello\" | \"Good morning\"}}",
"message": "<div>Hi {{FIRST_NAME | \"there\"}},</div><div><br /></div><div>This is an example cold email message. </div><div><br /></div><div>Best wishes, </div><div><a href=\"https://woodpecker.co\">Woodpecker</a> team</div><div><a href=\"{{UNSUBSCRIBE}}\">unsubscribe</a></div>",
"track_opens": true
}
]
}
}
}
}'
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
public class WoodpeckerApiClient {
private static final String API_URL = "https://api.woodpecker.co/rest/v2/campaigns";
public static void main(String[] args) {
try {
String apiKey = "YOUR_API_KEY"; // Replace the API key
HttpResponse<String> response = sendRequest(apiKey);
System.out.println("Response status code: " + response.statusCode());
System.out.println("Response body: " + response.body());
} catch (Exception e) {
e.printStackTrace();
}
}
private static HttpResponse<String> sendRequest(String apiKey) throws Exception {
ObjectMapper mapper = new ObjectMapper();
ObjectNode payload = createPayload(mapper);
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("x-api-key", apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toString()))
.build();
return client.send(request, HttpResponse.BodyHandlers.ofString());
}
private static ObjectNode createPayload(ObjectMapper mapper) {
ObjectNode payload = mapper.createObjectNode();
ArrayNode emailAccounts = payload.putArray("email_account_ids");
emailAccounts.add(112233); // Replace the SMTP id
ObjectNode settings = payload.putObject("settings");
settings.put("timezone", "Europe/Warsaw");
settings.put("daily_enroll", 40);
ObjectNode steps = payload.putObject("steps");
steps.put("type", "START");
ObjectNode followup = steps.putObject("followup");
followup.put("type", "EMAIL");
ObjectNode deliveryTime = followup.putObject("delivery_time");
ArrayNode monday = deliveryTime.putArray("MONDAY");
ObjectNode timeSlot = monday.addObject();
timeSlot.put("from", "08:00");
timeSlot.put("to", "17:00");
ObjectNode body = followup.putObject("body");
ArrayNode versions = body.putArray("versions");
ObjectNode version = versions.addObject();
version.put("subject", "Hi {{FIRST_NAME | \"there\"}}. {{SPINTAX | \"Hi\" | \"Hello\" | \"Good morning\"}}");
version.put("message", "<div>Hi {{FIRST_NAME | \"there\"}},</div><div><br /></div><div>This is an example cold email message. </div><div><br /></div><div>Best wishes, </div><div><a href=\"https://woodpecker.co\">Woodpecker</a> team</div><div><a href=\"{{UNSUBSCRIBE}}\">unsubscribe</a></div>");
version.put("track_opens", true);
return payload;
}
}
const axios = require("axios");
const API_KEY = "YOUR_API_KEY"; // Replace the API key
const API_URL = "https://api.woodpecker.co/rest/v2/campaigns";
const payload = {
email_account_ids: [112233], // Replace the SMTP id
settings: {
timezone: "Europe/Warsaw",
daily_enroll: 40,
},
steps: {
type: "START",
followup: {
type: "EMAIL",
delivery_time: {
MONDAY: [
{
from: "08:00",
to: "17:00",
},
],
},
body: {
versions: [
{
subject: 'Hi {{FIRST_NAME | "there"}}. {{SPINTAX | "Hi" | "Hello" | "Good morning"}}',
message:
'<div>Hi {{FIRST_NAME | "there"}},</div><div><br /></div><div>This is an example cold email message. </div><div><br /></div><div>Best wishes, </div><div><a href="https://woodpecker.co">Woodpecker</a> team</div><div><a href="{{UNSUBSCRIBE}}">unsubscribe</a></div>',
track_opens: true,
},
],
},
},
},
};
async function createCampaign() {
try {
const response = await axios.post(API_URL, payload, {
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
},
});
console.log("Status:", response.status);
console.log("Response:", response.data);
} catch (error) {
console.error("Error:", error.response?.data || error.message);
}
}
createCampaign();
Response
Response examples
- 201
- 400
- 401
- 404
- 409
- 500
Campaign created. The returned body will be a full campaign payload, which consists of any optional fields that were not included in the POST request, along with the following information:
id
- unique identifiers for the campaign, each step, and versionstatus
- status of the campaign, alwaysDRAFT
after creationversion
- version identifiers for each email, ranging fromA
toE
You can review the full campaign payload here.
Invalid request or malformed request syntax. For example missing array of email accounts. Please review the request body.
{
"code": "INPUT_DATA_VALIDATION_FAILURE",
"message": "Input data validation failure",
"details": null
}
Body schema
Field | Type | Description |
---|---|---|
code | string | Error code |
message | string | Descriptive error message |
details | string/null | Additional information. Currently always null |
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 |
Please review the request URL
{
"title": "Not Found",
"status": 404,
"detail": "Requested resource does not exist",
"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": null
}
Body schema
Field | Type | Description |
---|---|---|
code | string | Error code |
message | string | Descriptive error message |
details | string/null | Additional information. Currently always null |
Unknown error, please try again later.
{
"code": "UNKNOWN",
"message": "Unknown error during create campaign call",
"details": null
}
Body schema
Field | Type | Description |
---|---|---|
code | string | Error code |
message | string | Descriptive error message |
details | string/null | Additional information. Currently always null |