Add campaign step
This endpoint lets you add a new step to a campaign. The new step can only be added at the end of the campaign and cannot be inserted between existing steps. Additionally, it can only be added if the parent step has not yet processed any prospects.
Only campaigns with a status of DRAFT
or EDITED
can be updated. To change the campaign status to EDITED
use the /make_editable endpoint.
Request
Endpoint
POST https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps
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 order to fetch the parent_id
you can use the GET /campaigns structure endpoint
{
"parent_id": "9e8e8ebb-c6e1-4b8f-be37-a5542513dbge",
"step": {
"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": null,
"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": "NO_SIGNATURE",
"track_opens": false
}
]
},
"followup_after": {
"range": "HOUR",
"value": 10
}
}
}
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.
Step object
This object defines a step of a campaign, including its content, versions, delivery times, and follow-ups.
Field | Type | Default | Required | Description |
---|---|---|---|---|
type | string | - | YES | Only accepted value is EMAIL to indicate it is an email step` |
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; or the delay after which a prospect will be marked as NONRESPONSIVE |
└─range | string | DAY | NO | Time unit - DAY , HOUR , MINUTE |
└─value | integer | 1 | NO | Value of the time unit |
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 | - | NO | Email subject line. To send the email as a follow-up in the same thread, leave this field empty. If you enter a subject line, the email will be sent in a new 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/{campaign_id}/steps" \
--header "x-api-key: {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--data '{
"parent_id": "9e8e8ebb-c6e1-4b8f-be37-a5542513dbge",
"step": {
"type": "EMAIL",
"delivery_time": {
"TUESDAY": [
{
"from": "09:00",
"to": "18:00"
}
]
},
"body": {
"versions": [
{
"subject": null,
"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
}
]
},
"followup_after": {
"range": "HOUR",
"value": 10
}
}
}'
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
public class WoodpeckerApiClient {
public static void main(String[] args) {
String apiKey = "{YOUR_API_KEY}";
String campaignId = "{campaign_id}";
String apiUrl = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/steps";
try {
Map<String, Object> body = createRequestBody();
ObjectMapper objectMapper = new ObjectMapper();
String jsonBody = objectMapper.writeValueAsString(body);
int responseCode = sendPostRequest(apiUrl, apiKey, jsonBody);
System.out.println("Response Code: " + responseCode);
} catch (Exception e) {
e.printStackTrace();
}
}
private static Map<String, Object> createRequestBody() {
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("parent_id", "9e8e8ebb-c6e1-4b8f-be37-a5542513dbge");
// Step object
Map<String, Object> step = new HashMap<>();
step.put("type", "EMAIL");
// Delivery time
Map<String, List<Map<String, String>>> deliveryTime = new HashMap<>();
deliveryTime.put("TUESDAY", Collections.singletonList(Map.of("from", "09:00", "to", "18:00")));
step.put("delivery_time", deliveryTime);
// Email body
Map<String, Object> emailBody = new HashMap<>();
Map<String, Object> version = new HashMap<>();
version.put("subject", null);
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("signature", "SENDER");
version.put("track_opens", true);
emailBody.put("versions", Collections.singletonList(version));
step.put("body", emailBody);
// Follow-up after
Map<String, Object> followupAfter = new HashMap<>();
followupAfter.put("range", "HOUR");
followupAfter.put("value", 10);
step.put("followup_after", followupAfter);
requestBody.put("step", step);
return requestBody;
}
private static int sendPostRequest(String apiUrl, String apiKey, String jsonBody) throws Exception {
URL url = new URL(apiUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("x-api-key", apiKey);
connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true);
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonBody.getBytes("utf-8");
os.write(input, 0, input.length);
}
return connection.getResponseCode();
}
}
const axios = require("axios");
const API_KEY = "{YOUR_API_KEY}";
const CAMPAIGN_ID = "{campaign_id}";
const API_URL = `https://api.woodpecker.co/rest/v2/campaigns/${CAMPAIGN_ID}/steps`;
const requestBody = {
parent_id: "9e8e8ebb-c6e1-4b8f-be37-a5542513dbge",
step: {
type: "EMAIL",
delivery_time: {
TUESDAY: [{ from: "09:00", to: "18:00" }],
},
body: {
versions: [
{
subject: null,
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,
},
],
},
followup_after: {
range: "HOUR",
value: 10,
},
},
};
async function sendRequest() {
try {
const response = await axios.post(API_URL, requestBody, {
headers: {
"x-api-key": API_KEY,
"Content-Type": "application/json",
},
});
console.log("Response Status:", response.status);
console.log("Response Data:", response.data);
} catch (error) {
console.error("Error:", error.response ? error.response.data : error.message);
}
}
sendRequest();
Response
Response examples
- 201
- 400
- 401
- 404
- 409
- 500
The step has been added. A full campaign payload will be returned.
Invalid request or malformed syntax. Please review the request.
{
"code": "INPUT_DATA_VALIDATION_FAILURE",
"message": "Input data validation failure",
"details": {
"errors": [
{
"field": "Field name",
"detail": "Issue description"
}
]
}
}
Body schema
Field | Type | Description |
---|---|---|
code | string | Error code |
message | string | Descriptive error message |
details | object | Additional information |
└─errors | array[object] | An array of error objects. Each parameter with an issue will be listed |
└─[].field | string | Specifies the parameter with an issue |
└─[].detail | string | Description of the issue |
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 requested campaign doesn't exist.
{
"code": "CAMPAIGN_NOT_EXIST",
"message": "Campaign not found",
"details": null
}
Body schema
Field | Type | Description |
---|---|---|
code | string | Error code |
message | string | Descriptive error message |
details | string/null | Additional information. Currently always null |
The parent has either already processed some prospects, is not the final step of the campaign, or the campaign is in a status that prevents edits.
{
"code": "PARENT_BRANCH_NOT_LINKABLE" | "NOT_EDITABLE_STATUS",
"message": "Can not link new steps to this parent" | "The campaign must be in DRAFT or EDITED status to be updated",
"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 add step 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 |