# Woodpecker Docs > Woodpecker developer documentation for integrating with the public API, webhooks, MCP server, and CLI. Use these resources to manage campaigns, prospects, mailboxes, domains, reports, and other Woodpecker features programmatically. This file contains all documentation content in a single document following the llmstxt.org standard. ## Introduction The Woodpecker API provides a RESTful infrastructure that allows you to manage your account, prospect list, campaigns, reports, mailboxes, and client accounts as an agency. Our goal is to create an API that enables full external management of your and your client's Woodpecker accounts. If you have any suggestions, feel free to reach out to us at developers@woodpecker.co. ## Prerequisites Access to the API is a part of the `API keys & integrations` add-on. You can check your access [in the add-ons section](https://app.woodpecker.co/panel#add-ons). This feature is also available to all trial users. ## Support This documentation focuses on the Woodpecker API to help you connect with external tools or build your own integration. For queries related to the API, please contact us at developers@woodpecker.co. If you need help regarding the application functionalities and any non-API related topics, please visit our [Help Center](https://woodpecker.co/help-center/en/). ## Base URL The base URL for all endpoints is `https://api.woodpecker.co/rest` ## Authentication All requests must be authenticated using an API key in the `x-api-key` header. Read more [here](/docs/getting-started/authentication.mdx). ## Rate limiting Rate limits are in place to maintain reliable API access for all users. Read more [here](/docs/getting-started/rate-limiting.md) ## Account scopes Our API provides account-level endpoints accessible to all users. Additionally, enabling the [Agency add-on](#prerequisites) grants access to endpoints dedicated to managing multiple client accounts at [the agency level](/docs/agency-api/agency-api.md). --- ## Authentication All requests must be authenticated using an API key in the `x-api-key` header. ## Generating an API key To authenticate requests, you must first generate an API key. [Click here](https://app.woodpecker.co/panel#add-ons/integrations/api-keys) to go to the API keys view, or follow the instructions below: 1. Log into the Woodpecker account 2. Go to the Add-ons in the top-right corner → API & Integrations → 'API keys' 3. Click `Create a key` 4. You can add a label to each created key to describe what integration it is being used for API keys are user-specific, meaning each user only sees their own keys. Keep them private and do not share them with others. ## Authenticating requests The base URL is: ``` https://api.woodpecker.co/rest ``` All requests need to be authenticated using an `x-api-key` header. Try the request below, making sure to replace `{YOUR_API_KEY}` with your actual key. ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v1/me" \ --header "x-api-key: {YOUR_API_KEY}" ``` --- ## Error codes When interacting with our API, you may encounter error responses that follow standard HTTP status codes, providing meaningful feedback to help diagnose and resolve issues efficiently. While many errors use a default format, some endpoints return more specific messages or have slight variations in their response structure. Each endpoint description includes a detailed description of its possible error responses in the "Response" section of its documentation. Below, you'll find an overview of default error codes, and their meanings. Invalid request or malformed request syntax. Please review the request body ```json { "title": "Bad request", "status": 400, "detail": "Value of {field_name} is incorrect." | "Value of {field_name} is required." | "Your request was not valid. Please check the body for any mistakes.", "timestamp": "2025-03-05 17:57:00" } ``` An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "Invalid api key" | "No API addon" | "Upgrade your plan", "timestamp": "2025-03-05 17:57:00" } ``` Your current feature set doesn't grant access to this action. For example using agency endpoints without the Agency add-on ```json { "title": "Forbidden", "status": 403, "detail": "User do not have permissions for specified resource", "timestamp": "2025-03-05 17:57:00" } ``` Review the request URL for errors or confirm the specified resource exists ```json { "title": "Not Found", "status": 404, "detail": "Requested resource does not exist", "timestamp": "2025-03-05 17:57:00" } ``` Too many requests. Refer to the [rate limiting guide](rate-limiting.md) ```json { "title": "Too Many Requests", "status": 429, "detail": "Too many requests.", "timestamp": "2025-03-05 17:57:00" } ``` Unexpected error, please try again later ```json { "title": "Internal Server Error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Rate limiting Our API supports unlimited monthly calls while processing one request at a time and queuing up to 6 additional requests. If you exceed these limits, any extra requests will be dropped, and you will receive a `429 "Too Many Requests"` response. ### Rate Limiting Details: * **Concurrent processing:** One request is processed at a time. * **Queue limits:** Up to 6 additional requests can be queued, with a maximum wait time of 15 seconds per request. * Each queued request has a maximum wait time of 15 seconds, measured individually from the moment it enters the queue. * Any additional requests beyond this limit will be dropped. * **Account-based limits:** Limits are counted per account, not per API key. For agency users, each client account and HQ account maintain their own limits. --- ## Changelog ## September 2026 - September 8 - **Mailbox connection error details.** Identify mailbox connection issues more precisely with separate SMTP and IMAP error details in the [connection batch summary](/docs/mailboxes/get-batch-summary.mdx). - September 8 - **LinkedIn reply webhook.** Receive notifications with reply content when prospects respond to LinkedIn direct messages, InMail, or connection request messages. [View the webhook](/docs/webhooks/prospect-li-replied.mdx) - September 2 - **ChatGPT and Codex MCP connection.** In addition to Claude and Claude Code, you can now connect ChatGPT Desktop or Codex CLI to the hosted Woodpecker MCP server. [Connect ChatGPT or Codex](/docs/mcp/connect-openai.mdx) ## August 2026 - August 11 - **LinkedIn post engagement collection.** New API endpoints let you collect profiles and engagement data of people who reacted to or commented on LinkedIn posts, save this information to a new Woodpecker list, and enrich the collected profiles with contact data. [Collect profiles from posts](/docs/linkedin/collecting-profiles/post-collect-profiles.mdx) ## July 2026 - July 30 - **Hosted Woodpecker MCP server.** Connect Claude or Claude Code to Woodpecker through the hosted MCP server and a browser-based authorization flow. [Connect the MCP server](/docs/mcp/connect-claude.mdx) - July 30 - **Working-day follow-up delays.** Campaign API v2 now supports `count_followup_delay_in_working_days`, which skips Saturdays and Sundays when calculating follow-up delays. [See campaign API v2](/docs/campaigns/campaigns.mdx) - July 16 - **Mailbox connection diagnostics.** Mailbox responses now include IMAP errors and a `reconnect_required` flag for SMTP mailboxes. [Review mailbox responses](/docs/mailboxes/get-mailboxes.mdx) - July 3 - **Domains API.** The new API covers the complete domain and mailbox setup workflow, from finding and ordering domains to monitoring their status and managing their configuration. [Explore the Domains API](/docs/domains/domains.md) ## June 2026 - June 17 - **Microsoft 365 mailbox management.** New endpoints let you manage Microsoft Graph app-only credentials and connect multiple Microsoft 365 mailboxes in one request. [Connect Microsoft mailboxes](/docs/mailboxes/microsoft/overview.md) - June 17 - **Bounce Shield API.** New endpoints let you retrieve, set, or clear the bounce-rate threshold that automatically pauses a campaign. [Configure Bounce Shield](/docs/campaigns/bounce-shield/overview.md) - June 11 - **LinkedIn account lifecycle webhooks.** New webhooks report when a LinkedIn account is [connected](/docs/webhooks/linkedin-account-connected.mdx) or [disconnected](/docs/webhooks/linkedin-account-disconnected.mdx). - June 2 - **Bounce Shield pause webhook.** A new webhook reports when Bounce Shield pauses a campaign and returns the campaign details and configured threshold. [View the webhook](/docs/webhooks/campaign-paused-by-bounce-shield.mdx) ## May 2026 - May 27 - **Lead Finder API.** New endpoints let you search for leads, enrich selected results, or enrich prospects already stored in Woodpecker. Enrichment is processed asynchronously in batches. [Explore Lead Finder](/docs/lead-finder/lead-finder.md) - May 25 - **Campaign auto-pause settings.** Campaign API v2 can pause other prospects from the same domain after a `REPLIED` or `BOUNCED` status. Campaign responses also include the timestamp of an automatic Bounce Shield pause. [See campaign API v2](/docs/campaigns/campaigns.mdx) - May 12 - **Quoted replies in the Inbox API.** The boolean `quote_original_message` field lets you include or omit the original inbox message below a reply. [Reply to an inbox message](/docs/inbox/post-reply-message.mdx) - May 7 - **LinkedIn direct message webhook.** A new webhook reports sent LinkedIn direct messages with prospect, campaign, sender, and message details. [View the webhook](/docs/webhooks/prospect-li-dm-sent.mdx) - May 6 - **LinkedIn InMail campaign steps.** Campaign API v2 now supports `INMAIL_MESSAGE` steps, including subjects and message variants. [See campaign API v2](/docs/campaigns/campaigns.mdx) - May 5 - **Woodpecker CLI.** Bring Woodpecker to the terminal for quick checks, scripts, and a structured way for AI coding agents to work with your account. [Get started with the CLI](/docs/cli/cli.mdx) ## February 2026 - February 28 - **LinkedIn connection request webhook.** A new webhook reports when a prospect accepts an automated LinkedIn connection request. [View the webhook](/docs/webhooks/prospect-li-cr-accepted.mdx) - February 12 - **LinkedIn account connection for agencies.** New Agency API endpoints let you create or reconnect LinkedIn accounts for client companies and generate authorization links. [Connect LinkedIn accounts](/docs/agency-api/linkedin/linkedin.md) - February 8 - **Inbox API update.** The Inbox API now supports message search, additional filters, cursor-based pagination in both directions, and expanded campaign and prospect data. [Explore the Inbox API](/docs/inbox/inbox.md) ## October 2025 - October 22 - **Mailbox footer management.** API v2 now supports adding, replacing, or removing the HTML footer of a connected SMTP mailbox. [Update a mailbox](/docs/mailboxes/update-mailbox.mdx) - October 7 - **LinkedIn steps in campaign API v2.** Campaigns can now include LinkedIn profile visits, connection requests, and direct messages alongside email steps. [See campaign API v2](/docs/campaigns/campaigns.mdx) ## September 2025 - September 30 - **LinkedIn accounts endpoint.** A new endpoint lists LinkedIn accounts connected to Woodpecker, including their session status, user details, and subscription level. [List LinkedIn accounts](/docs/linkedin/get-linkedin-accounts.mdx) ## May 2025 - May 13 - **AI-detected Interest Levels in API v1 and webhooks.** Prospect responses in API v1 and prospect-related webhooks now include an `interest_level` object with the assigned level and whether it was detected by AI. [View the Prospect interested webhook](/docs/webhooks/prospect-interested.mdx) --- ## Blacklisting This feature allows you to specify domains and emails that you don't want to contact again as long as they remain on the blacklist. Prospects whose emails are blacklisted or belong to a given domain will have their status automatically changed to `BLACKLISTED` during the processing of the campaign and won't receive further communication. ## Blacklisting emails Blacklisting an email address prevents any campaign from contacting that specific prospect. You can add, retrieve, and remove blacklisted emails via the API. ## Blacklisting domains You can also blacklist entire domains or domains matching a specific pattern. Use % as a wildcard, similar to the regex pattern .*: * `domain%` covers `domain-a.com`, `domain-b.org`, etc. * `%domain%.%` covers `getdomainnow.io`, `trydomainfree.com`, etc. --- ## Delete domains from the blacklist Remove specific domains from the blacklist. Removing a domain from the list doesn't change the status of a prospect if it was previously set to `BLACKLISTED`. ## Request ### Endpoint ``` DELETE https://api.woodpecker.co/rest/v2/blacklist/domains ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body :::info You can remove up to 500 domains per request ::: ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } ``` | Field | Type | Description | | ------------- | ------ | --------------------------- | | `domains` | array[string] | List of domains to remove from blacklist | ### Request samples #### Remove a list of domains from blacklist ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/blacklist/domains" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] }' ``` ```Python import requests def deleteBlacklistedDomains(): url = "https://api.woodpecker.co/rest/v2/blacklist/domains" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } response = requests.delete(url, headers=headers, json=payload) if response.status_code == 200: print("DELETE successful:", response.json()) else: print("DELETE failed with status:", response.status_code) if __name__ == "__main__": deleteBlacklistedDomains() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { deleteBlacklistedDomains(); } public static void deleteBlacklistedDomains() { try { String url = "https://api.woodpecker.co/rest/v2/blacklist/domains"; String jsonData = "{" + "\"domains\": [" + "\"baddomain.com\"," + "\"blacklistedomain.io\"," + "\"nomoreemails.co\"" + "]" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .method("DELETE", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("DELETE response: " + response.body()); } else { System.err.println("DELETE request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function deleteBlacklistedDomains() { const url = "https://api.woodpecker.co/rest/v2/blacklist/domains"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { domains: [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] }; try { const response = await axios.delete(url, { headers: headers, data: data }); if (response.status === 200) { console.log("DELETE successful:", response.data); } else { console.error("DELETE failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } deleteBlacklistedDomains(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->delete('blacklist/domains', [ 'json' => [ 'domains' => [ 'baddomain.com', 'blacklistedomain.io', 'nomoreemails.co', ], ], ]); 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 Returns a list of successfully removed domains, including only those that were previously blacklisted; domains not found in the blacklist or with an invalid format are ignored and not included in the response. If none of the requested domains were blacklisted, the returned array will be empty. ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } ``` #### Body schema | Field | Type | Description | | --------- | ------------- | --------------------------- | | `domains` | array[string] | List of domains removed from the blacklist | Invalid request or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "You can proceed with up to 500 elements in one request" | "Domains parameter can not be empty" | "Value of domains is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Delete emails from the blacklist Remove specific emails from the blacklist. Removing an email from the list doesn't change the status of a prospects if it was previously set to `BLACKLISTED` ## Request ### Endpoint ``` DELETE https://api.woodpecker.co/rest/v2/blacklist/emails ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body :::info You can remove up to 500 emails per request ::: ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } ``` | Field | Type | Description | | -------- | ------------- | -------------------------- | | `emails` | array[string] | List of emails to remove from blacklist | ### Request samples #### Remove a list of emails from blacklist ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/blacklist/emails" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] }' ``` ```Python import requests def delete_blacklist_emails(): url = "https://api.woodpecker.co/rest/v2/blacklist/emails" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } response = requests.delete(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"DELETE request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = delete_blacklist_emails() print("DELETE response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/blacklist/emails"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); String jsonPayload = "{\"emails\": [\"wrong@baddomain.com\", \"worse@anotherone.com\", \"john@finisheddeal.co.uk\"]}"; HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("Content-Type", "application/json") .header("x-api-key", API_KEY) .method("DELETE", HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("DELETE response: " + response.body()); } else { System.err.println("DELETE request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function deleteBlacklistEmails() { const url = 'https://api.woodpecker.co/rest/v2/blacklist/emails'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { emails: [ 'wrong@baddomain.com', 'worse@anotherone.com', 'john@finisheddeal.co.uk' ] }; try { const response = await axios.delete(url, { headers, data }); if (response.status === 200) { console.log('DELETE response:', response.data); } else { console.error('DELETE request failed:', response.status); } } catch (error) { console.error('DELETE request failed:', error.response ? error.response.status : error.message); } } deleteBlacklistEmails(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->delete('blacklist/emails', [ 'json' => [ 'emails' => [ 'wrong@baddomain.com', 'worse@anotherone.com', 'john@finisheddeal.co.uk', ], ], ]); 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 Returns a list of successfully removed emails, including only those that were previously blacklisted; emails not found in the blacklist or with an invalid format are ignored and not included in the response. If none of the requested emails were blacklisted, the returned array will be empty. ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } ``` #### Body schema | Field | Type | Description | | --------- | ------------- | --------------------------- | | `emails` | array[string] | List of emails removed from the blacklist | Invalid request or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "You can proceed with up to 500 elements in one request" | "Emails parameter can not be empty" | "Value of emails is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Get blacklisted domains Retrieve a paginated list of all blacklisted domains for the authenticated account. You can use the `domain_filter` parameter to check whether specific domains are included in the blacklist. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/blacklist/domains ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Description | | ---------- | -------- | ------------------------------------------------------- | | `page` | No | Requested results page | | `per_page` | No | Number of records per page. Default: 100, maximum: 500 | | `domain_filter` | No | Comma-separated domains to check against the list. Use `*` as a wildcard - `woodpecker*` will match `woodpecker.co` and `woodpeckers.tld` | ### Request samples #### Retrieve first 500 domains ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/blacklist/domains?page=1&per_page=500" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getBlacklistedDomains(): url = "https://api.woodpecker.co/rest/v2/blacklist/domains?page=1&per_page=500" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getBlacklistedDomains() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getBlacklistedDomains(); } public static void getBlacklistedDomains() { try { String url = "https://api.woodpecker.co/rest/v2/blacklist/domains?page=1&per_page=500"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getBlacklistedDomains() { const url = "https://api.woodpecker.co/rest/v2/blacklist/domains?page=1&per_page=500"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getBlacklistedDomains(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('blacklist/domains', [ 'query' => [ 'page' => 1, 'per_page' => 500, ], ]); 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 A list of blacklisted domains. The domains are sorted alphabetically. ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "finisheddeal.co.uk", "nomoreemails.co", "notmyicp.design" ], "total": 5 } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `domains` | array[string] | List of blacklisted domains | | `total` | integer | Total number of blacklisted domains, or total number of found domains when using `domain_filter` | Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Value of {field_name} is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Get blacklisted emails Retrieve a paginated list of all blacklisted emails for the authenticated account. You can use the `email_filter` parameter to check whether specific emails are included in the blacklist. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/blacklist/emails ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Description | | ---------- | -------- | ------------------------------------------------------- | | `page` | No | Requested results page | | `per_page` | No | Number of records per page. Default: 100, maximum: 500 | | `email_filter` | No | Comma-separated emails to check against the list. Use `*` as a wildcard - `jimothy@woodpecker*` will match `jimothy@woodpecker.co` and `jimothy@woodpeckers.tld` | ### Request samples #### Retrieve first 500 emails ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/blacklist/emails?page=1&per_page=500" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getBlacklistedEmails(): url = "https://api.woodpecker.co/rest/v2/blacklist/emails?page=1&per_page=500" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getBlacklistedEmails() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getBlacklistedEmails(); } public static void getBlacklistedEmails() { try { String url = "https://api.woodpecker.co/rest/v2/blacklist/emails?page=1&per_page=500"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getBlacklistedEmails() { const url = "https://api.woodpecker.co/rest/v2/blacklist/emails?page=1&per_page=500"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getBlacklistedEmails(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('blacklist/emails', [ 'query' => [ 'page' => 1, 'per_page' => 500, ], ]); 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 A list of blacklisted emails ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk", "drew@nomoreemails.co", "andrew@notmyicp.design" ], "total": 5 } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `emails` | array[string] | List of blacklisted emails | | `total` | integer | Total number of blacklisted emails, or total number of found emails when using `email_filter` | Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Value of {field_name} is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Blacklist domains Add domains to the blacklist. Blacklisting a domain does not immediately change the status of existing prospects. Instead, it prevents contacting prospects associated with that domain across all campaigns. A prospect's status will be updated to `BLACKLISTED` during campaign processing. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/blacklist/domains ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body :::info You can add up to 500 domains per request. ::: ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } ``` | Field | Type | Description | | ------------- | ------ | --------------------------- | | `domains` | array[string] | List of domains to blacklist | ### Request samples #### Blacklist a list of domains ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/blacklist/domains" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] }' ``` ```Python import requests def addBlacklistedDomains(): url = "https://api.woodpecker.co/rest/v2/blacklist/domains" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": addBlacklistedDomains() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { addBlacklistedDomains(); } public static void addBlacklistedDomains() { try { String url = "https://api.woodpecker.co/rest/v2/blacklist/domains"; String jsonData = "{" + "\"domains\": [" + "\"baddomain.com\"," + "\"blacklistedomain.io\"," + "\"nomoreemails.co\"" + "]" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function addBlacklistedDomains() { const url = "https://api.woodpecker.co/rest/v2/blacklist/domains"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { domains: [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } addBlacklistedDomains(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('blacklist/domains', [ 'json' => [ 'domains' => [ 'baddomain.com', 'blacklistedomain.io', 'nomoreemails.co', ], ], ]); 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 Returns a list of successfully blacklisted domains, including those newly added and those already blacklisted. ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } ``` #### Body schema | Field | Type | Description | | --------- | ------------- | --------------------------- | | `domains` | array[string] | List of blacklisted domains | Invalid request body or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Domains parameter can not be empty" | "You can proceed with up to 500 elements in one request" | "All of passed domains were invalid" | "Value of domains is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Blacklist emails Add emails to the blacklist. Blacklisting an email does not immediately change the status of existing prospects. Instead, it prevents contacting specific prospects across all campaigns. A prospect's status will be updated to `BLACKLISTED` during campaign processing. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/blacklist/emails ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body :::info You can add up to 500 emails per request ::: ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } ``` | Field | Type | Description | | ------------- | ------ | --------------------------- | | `emails` | array[string] | List of emails to blacklist | ### Request samples #### Blacklist a list of emails ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/blacklist/emails" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] }' ``` ```Python import requests def blacklist_emails(): url = "https://api.woodpecker.co/rest/v2/blacklist/emails" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = blacklist_emails() print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/blacklist/emails"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); String jsonPayload = "{\"emails\": [\"wrong@baddomain.com\", \"worse@anotherone.com\", \"john@finisheddeal.co.uk\"]}"; HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("Content-Type", "application/json") .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function blacklistEmails() { const url = 'https://api.woodpecker.co/rest/v2/blacklist/emails'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { emails: [ 'wrong@baddomain.com', 'worse@anotherone.com', 'john@finisheddeal.co.uk' ] }; try { const response = await axios.post(url, data, { headers }); if (response.status === 200) { console.log('POST response:', response.data); } else { console.error('POST request failed:', response.status); } } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } blacklistEmails(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('blacklist/emails', [ 'json' => [ 'emails' => [ 'wrong@baddomain.com', 'worse@anotherone.com', 'john@finisheddeal.co.uk', ], ], ]); 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 Returns a list of successfully blacklisted emails, including those newly added and those already blacklisted. ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } ``` #### Body schema | Field | Type | Description | | --------- | ------------- | --------------------------- | | `emails` | array[string] | List of blacklisted emails | Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Emails parameter can not be empty" | "You can proceed with up to 500 elements in one request" | "All of passed emails were invalid" | "Value of emails is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Delete campaign step This endpoint lets you delete a campaign step. You can delete `EMAIL` and `LINKEDIN` steps that haven't processed any prospects and are not the first step in the campaign following the `START` step. Once a step is deleted, it cannot be restored. Only campaigns with a status of `DRAFT` or `EDITED` can be updated. To change the campaign status to `EDITED` use the [/make_editable endpoint](POST-editable-campaign.mdx). ## Request ### Endpoint ``` DELETE https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id} ``` You can fetch the `step_id` using the [GET /campaigns structure endpoint](GET-campaign.mdx). ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Delete a campaign step ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def deleteCampaignStep(campaign_id, step_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.delete(url, headers=headers) if response.status_code == 200: print("DELETE successful:", response.json()) else: print("DELETE failed with status:", response.status_code) if __name__ == "__main__": deleteCampaignStep(123, 456) # Example IDs ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; // Example campaign ID int stepId = 456; // Example step ID deleteCampaignStep(campaignId, stepId); } public static void deleteCampaignStep(int campaignId, int stepId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/steps/" + stepId; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("DELETE response: " + response.body()); } else { System.err.println("DELETE request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function deleteCampaignStep(campaignId, stepId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/steps/${stepId}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.delete(url, { headers: headers }); if (response.status === 200) { console.log("DELETE successful:", response.data); } else { console.error("DELETE failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } deleteCampaignStep(123, 456); // Example IDs ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $campaignId = '{campaign_id}'; $stepId = '{step_id}'; try { $response = $client->delete("campaigns/{$campaignId}/steps/{$stepId}"); 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 The campaign step has been deleted. A [full campaign payload](campaigns.mdx#campaign-body-schema) will be returned. An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 or step doesn't exist. ```json { "code": "CAMPAIGN_NOT_EXIST" | "BRANCH_NOT_FOUND", "message": "Campaign not found" | "Step not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | The step has already processed some prospects, is the first step following `START` step, is a `START` step, or the campaign is in a status that prohibits edits. ```json { "code": "BRANCH_NOT_DELETABLE" | "NOT_EDITABLE_STATUS", "message": "Step is not allowed to be deleted" | "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 | Error message | | `details` | string/null | Additional information | An unknown error while deleting the campaign. Please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during delete campaign step call", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | --- ## Delete campaign This request will delete a campaign. You can delete a campaign in any status, except if it is part of a [workflow](https://woodpecker.co/help-center/en/articles/6811128). Once deleted, a campaign cannot be restored. ## Request ### Endpoint ``` DELETE https://api.woodpecker.co/rest/v2/campaigns/{campaign_id} ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Stop a campaign ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def deleteCampaign(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.delete(url, headers=headers) if response.status_code == 200: print("DELETE successful:", response.status_code) else: print("DELETE failed with status:", response.status_code) if __name__ == "__main__": deleteCampaign(123) # Example campaign ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; // Example campaign ID deleteCampaign(campaignId); } public static void deleteCampaign(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("DELETE response: " + response.statusCode()); } else { System.err.println("DELETE request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function deleteCampaign(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.delete(url, { headers: headers }); if (response.status === 200) { console.log("DELETE successful:", response.status); } else { console.error("DELETE failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } deleteCampaign(123); // Example campaign ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $campaignId = '{campaign_id}'; try { $response = $client->delete("campaigns/{$campaignId}"); 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 The campaign has been deleted. ``` Status: 200 Body: none ``` An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | The requested campaign is a part of an existing [workflow](https://woodpecker.co/help-center/en/articles/6811128). If you wish to delete this campaign - remove the workflow first. ```json { "code": "CAMPAIGN_RELATED_TO_ACTIVE_WORKFLOW", "message": "Campaign related to active workflow", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An unknown error while deleting the campaign. Please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during delete campaign call", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | --- ## Get a list of campaigns :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: Fetch all campaigns associated with your account, with the option to filter by status for more targeted results. The response includes basic campaign information. You can use the campaign ID further to: * fetch campaign statistics - see `rest/v1/campaign_list?id={id}` endpoint [here](GET-campaign-stats-v1.mdx) * fetch campaign settings and content - see `v2/campaigns/{id}` endpoint [here](GET-campaign.mdx) ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v1/campaign_list ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Description | | --------- | -------- | ------------------------------------------------------------------------------------------- | | `status` | No | Filter campaigns by their status. Comma-separated list of statuses: `RUNNING`, `DRAFT`, `EDITED`, `PAUSED`, `STOPPED`, `COMPLETED` | | `id` | No | Comma-separated list of campaign `id`s. Requesting only one id returns detailed information about it; see [dedicated article](GET-campaign-stats-v1.mdx) for more information | ### Request samples #### Fetch RUNNING and PAUSED campaigns ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v1/campaign_list?status=RUNNING,PAUSED" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getCampaignList(): url = "https://api.woodpecker.co/rest/v1/campaign_list?status=RUNNING,PAUSED" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getCampaignList() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getCampaignList(); } public static void getCampaignList() { try { String url = "https://api.woodpecker.co/rest/v1/campaign_list?status=RUNNING,PAUSED"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getCampaignList() { const url = "https://api.woodpecker.co/rest/v1/campaign_list?status=RUNNING,PAUSED"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getCampaignList(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->get('campaign_list', [ 'query' => [ 'status' => 'RUNNING,PAUSED', ], ]); 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 An array of all campaigns meeting your criteria. ```json [ { "id": 1234567, "name": "SaaS CEOs", "status": "RUNNING", "created": "2025-02-10T13:14:57+0100", "from_name": "Erlich Bachman", "from_names": ["Erlich Bachman", "Jared Dunn", "Richard Hendricks", "Jian"], "gdpr_unsubscribe": false, "folder_name": "SaaS in America", "folder_id": 987, "from_email": "erlich.bachman@piedpiper.com", "from_emails": [ "erlich.bachman@piedpiper.com", "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "per_day": 35, "bcc": "", "cc": "", "error": "" }, { "id": 1235678, "name": "Test campaign", "status": "COMPLETED", "created": "2025-02-03T16:10:11+0100", "from_name": "Erlich Bachman", "from_names": ["Erlich Bachman", "Jared Dunn"], "gdpr_unsubscribe": true, "folder_name": "Finished campaigns", "folder_id": 954, "from_email": "erlich.bachman@piedpiper.com", "from_emails": [ "erlich.bachman@piedpiper.com", "jared.dunn@piedpiper.com" ], "per_day": 35, "bcc": "sentemails@crm.com", "cc": "", "error": "" } ] ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `id` | integer | Unique identifier of the campaign | | `name` | string | Name of the campaign | | `status` | string | Current campaign status. Possible values: `RUNNING`, `DRAFT`, `STOPPED`, `PAUSED`, `EDITED`, `COMPLETED` | | `created` | string | Campaign creation date (ISO 8601) | | `from_name` | string | One of the sending emails 'from name'. If multiple are used, refer to `from_names` instead | | `from_names` | array[string] | A list of sender names used in the campaign | | `gdpr_unsubscribe` | boolean | Whether GDPR-compliant unsubscribe is enabled | | `folder_name` | string | Name of the folder the campaign is assigned to | | `folder_id` | integer | ID of the folder the campaign is assigned to. `0` stands for general `UNASSIGNED` folder | | `from_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `from_emails` instead | | `from_emails` | array[string] | List of campaign sending email addresses | | `per_day` | integer | Maximum number of prospects that can be contacted in the opening step of the campaign per day. This limit is applied per mailbox or LinkedIn account | | `bcc` | string | Email address that receives a blind copy of outgoing messages | | `cc` | string | Email address that receives a carbon copy of outgoing messages | | `error` | deprecated | Deprecated. Empty string | There are no campaigns matching your criteria ``` Status: 204 Body: None ``` Invalid request or malformed request syntax. Please review the [request](#request) ```json { "status": { "status": "ERROR", "code": "E_WRONG_PARAM", "msg": "Wrong param [status]=active" | "Unknown param:state" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Get campaign statistics :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: Retrieve campaign statistics. Use this endpoint to analyze the performance of a specific cold email campaign, including delivery, open, reply, bounce, and opt-out rates. Understanding the response: * Step versions - the response includes only the `A` version for email content and step settings, while performance metrics reflect data from all versions combined * LinkedIn automation and manual tasks are not supported by this endpoint, they are omitted from the response If you're looking for: * More campaign statistics - supplement your data with [predefined reports](/docs/reports/reports.md) * A detailed campaign structure including all campaign settings - see `v2/campaigns/{id}` endpoint [here](GET-campaign.mdx) * A list of all campaigns - see `v1/campaign_list` endpoint [here](GET-campaign-list-v1.mdx) ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v1/campaign_list?id={campaign_id} ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Description | | --------- | -------- | ------------------------------------------------------------------------------------------- | | `id` | Yes | Specify a single campaign `id` to retrieve detailed campaign information. Using multiple IDs [returns a list of campaigns](GET-campaign-list-v1.mdx) without statistics. | ### Request samples #### Fetch a campaign and its statistics ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v1/campaign_list?id={campaign_id}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getCampaignById(campaign_id): url = f"https://api.woodpecker.co/rest/v1/campaign_list?id={campaign_id}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getCampaignById(123) # Example campaign ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; // Example campaign ID getCampaignById(campaignId); } public static void getCampaignById(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v1/campaign_list?id=" + campaignId; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getCampaignById(campaignId) { const url = `https://api.woodpecker.co/rest/v1/campaign_list?id=${campaignId}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getCampaignById(123); // Example campaign ID ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); $campaignId = '{campaign_id}'; try { $response = $client->get('campaign_list', [ 'query' => [ 'id' => $campaignId, ], ]); 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 An array of all campaigns meeting your criteria. ```json [ { "id": 1234567, "name": "SQL follow-ups", "status": "RUNNING", "folder_name": "SaaS in America", "folder_id": 987, "from_name": "Erlich Bachman", "from_names": ["Erlich Bachman", "Jared Dunn", "Richard Hendricks", "Jian"], "gdpr_unsubscribe": true, "created": "2025-02-10T13:14:57+0100", "per_day": 35, "from_email": "erlich.bachman@piedpiper.com", "from_emails": ["erlich.bachman@piedpiper.com", "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com"], "bcc": "sentemails@crm.com", "cc": "", "stats": { "prospects": 868, "delivery": 853, "invalid": 6, "bounced": 4, "queue": 3, "sent": 857, "check": 21, "autoreplied": 0, "opened": 286, "optout": 3, "clicked": 0, "replied": 74, "interested": 42, "maybe_later": 11, "not_interested": 13, "emails": [ { "subject": "Example subject line", "msg": "

Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message.

Best wishes,
Woodpecker team

", "timezone": "Europe/Warsaw", "use_prospect_timezone": false, "sunFrom": null, "sunTo": null, "monFrom": null, "monTo": null, "tueFrom": null, "tueTo": null, "wedFrom": null, "wedTo": null, "thuFrom": null, "thuTo": null, "friFrom": null, "friTo": null, "satFrom": null, "satTo": null, "sunday": [{ "from": -1, "to": -1 }], "monday": [{ "from": 360, "to": 720 }, { "from": 900, "to": 990 }], "tuesday": [{ "from": 360, "to": 720 }, { "from": 900, "to": 990 }], "wednesday": [{ "from": 360, "to": 720 }], "thursday": [{ "from": 360, "to": 720 }], "friday": [{ "from": 360, "to": 720 }], "saturday": [{ "from": -1, "to": -1 }], "track_open": true, "track_click": false, "attach_follow": false, "follow_up": 0, "number": 1, "step": 1, "condition": null, "emailSend": 857, "toSend": 3, "delivery": 854, "open_": "29.3%", "open": 250, "reply_": "2.9%", "reply": 25, "invalid_": "0.0%", "invalid": 0, "bounce_": "0.4%", "bounce": 3 }, { "subject": "Re: Example subject line", "msg": "
First followup
", "timezone": "Europe/Warsaw", "use_prospect_timezone": false, "sunFrom": null, "sunTo": null, "monFrom": null, "monTo": null, "tueFrom": null, "tueTo": null, "wedFrom": null, "wedTo": null, "thuFrom": null, "thuTo": null, "friFrom": null, "friTo": null, "satFrom": null, "satTo": null, "sunday": [{ "from": -1, "to": -1 }], "monday": [{ "from": 360, "to": 720 }, { "from": 900, "to": 990 }], "tuesday": [{ "from": 360, "to": 720 }, { "from": 900, "to": 990 }], "wednesday": [{ "from": 360, "to": 720 }], "thursday": [{ "from": 360, "to": 720 }], "friday": [{ "from": 360, "to": 720 }], "saturday": [{ "from": -1, "to": -1 }], "track_open": false, "track_click": false, "attach_follow": false, "follow_up": 0, "number": 3, "step": 2, "condition": { "operator": "", "values": [ { "type": "PROSPECT_FIRST_NAME", "operand": "EXISTS", "value": "" } ] }, "emailSend": 818, "toSend": 10, "delivery": 818, "open_": "24.1%", "open": 197, "reply_": "3.4%", "reply": 28, "invalid_": "0.0%", "invalid": 0, "bounce_": "0.0%", "bounce": 0 }, { "subject": "Re: Example subject line", "msg": "
Second followup
", "timezone": "Europe/Warsaw", "use_prospect_timezone": false, "sunFrom": null, "sunTo": null, "monFrom": null, "monTo": null, "tueFrom": null, "tueTo": null, "wedFrom": null, "wedTo": null, "thuFrom": null, "thuTo": null, "friFrom": null, "friTo": null, "satFrom": null, "satTo": null, "sunday": [{ "from": -1, "to": -1 }], "monday": [{ "from": 360, "to": 720 }, { "from": 900, "to": 990 }], "tuesday": [{ "from": 360, "to": 720 }, { "from": 900, "to": 990 }], "wednesday": [{ "from": 360, "to": 720 }], "thursday": [{ "from": 360, "to": 720 }], "friday": [{ "from": 360, "to": 720 }], "saturday": [{ "from": -1, "to": -1 }], "track_open": false, "track_click": false, "attach_follow": false, "follow_up": 0, "number": 7, "step": 3, "condition": null, "emailSend": 546, "toSend": 25, "delivery": 545, "open_": "16.0%", "open": 87, "reply_": "4.0%", "reply": 22, "invalid_": "0.0%", "invalid": 0, "bounce_": "0.2%", "bounce": 1 } ] }, "error": "", "timestamp": "2025-03-01T15:00:30+0100" } ] ``` #### Body schema | Field | Data Type | Description | |----------------|----------------------|-------------| | `id` | integer | Unique identifier of the campaign | | `name` | string | Name of the campaign | | `status` | string | Current campaign status. Possible values: `RUNNING`, `DRAFT`, `STOPPED`, `PAUSED`, `EDITED`, `COMPLETED` | | `folder_name` | string | Name of the folder the campaign is assigned to | | `folder_id` | integer | ID of the folder the campaign is assigned to. 0 stands for general UNASSIGNED folder | | `from_name` | string | One of the sending emails 'from name'. If multiple are used, refer to `from_names` instead | | `from_names` | array[string] | A list of sender names used in the campaign | | `gdpr_unsubscribe` | boolean | Whether GDPR-compliant unsubscribe is enabled | | `created` | string | Campaign creation date (ISO 8601) | | `per_day` | integer | Maximum number of prospects that can be contacted in the opening step of the campaign per day. This limit is applied per mailbox or LinkedIn account | | `from_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `from_emails` instead | | `from_emails` | array[string] | List of campaign sending email addresses | | `bcc` | string | Email address that receives a blind copy of outgoing messages | | `cc` | string | Email address that receives a carbon copy of outgoing messages | | `stats` | object | Object holding campaign-level statistics | |   └─`prospects` | integer | Number of prospects added to the campaign | |   └─`delivery` | integer | Number of emails successfully delivered opening emails | |   └─`invalid` | integer | Number of invalid email addresses | |   └─`bounced` | integer | Number of prospects marked as `BOUNCED` | |   └─`queue` | integer | Number of prospects queued to receive the opening email | |   └─`sent` | integer | Total number of contacted prospects | |   └─`check` | integer | Number of prospects marked as `to check` (except manual pause) | |   └─`autoreplied` | integer | Number of prospects marked as `AUTOREPLIED` | |   └─`opened` | integer | Number of prospects who opened an email | |   └─`optout` | integer | Number of prospects who opted out | |   └─`clicked` | integer | Number of prospects who clicked a tracked link | |   └─`replied` | integer | Number of prospects who replied | |   └─`interested` | integer | Number of "interested" responses | |   └─`maybe_later` | integer | Number of "maybe later" responses | |   └─`not_interested` | integer | Number of "not_interested" responses | |   └─`emails` | array[objects] | An array of email objects. Each object being the `A` version of a campaign steps. [More details below](#emails-object-body-schema) | | `[].error` | deprecated | Deprecated. Empty string | | `[].timestamp` | string | Timestamp of sending the request (ISO 8601)| #### Emails object body schema The email object holds step-specific statistics like sent, delivered, opened, replied, bounced, and invalid email counts, along with open and reply rates. It also includes basic details like the subject line and schedule. The array order follows the campaign sequence, with the first element representing the first step. | Field | Data Type | Description | |-----------------------|------------------|-------------| | `subject` | string | Email subject line | | `msg` | string | Email body content in HTML format | | `timezone` | string | The default timezone of a campaign. It will be used when `use_prospect_timezone` is disabled or when it is enabled but the prospect's timezone is not specified | | `use_prospect_timezone` | boolean | Whether to adjust sending times to prospect's timezone instead of the campaign `timezone` | | `track_open` | boolean | Whether to track email opens for this email step version | | `track_click` | boolean | Whether this email step version contains a tracked link | | `attach_follow` | boolean | Deprecated. Always `false`| | `follow_up` | integer | Deprecated. Always `0` | | `number` | integer | Deprecated | | `step` | integer | Step order in the email sequence | | `condition` | object/null | An IF-condition that will evaluate the prospect's YES/NO path after this step | |   └─`operator` | string | Empty string | |   └─`values` | array[object] | Array containing condition details | |     └─`[].type` | string | Type of condition. `OPEN`, `CLICK`, `PROSPECT_{SNIPPET_NAME}` | |     └─`[].operand` | string | `MORE_THAN`, `EXISTS`, `EQUALS`, `CONTAINS` | |     └─`[].value` | string | Prospect value that will be evaluated against the type and operand of the condition. | | `emailSend` | integer | Number of emails sent in this step | | `toSend` | integer | Number of emails remaining to be sent in this step | | `delivery` | integer | Number of delivered emails in this step | | `open_` | string | Open rate percentage for this step (formatted as astring) | | `open` | integer | Number of prospects who have opened an email in this step | | `reply_` | string | Reply rate percentage (formatted as astring) | | `reply` | integer | Number of prospects who have replied to an email from this step | | `invalid_` | string | Percentage of emails marked as `INVALID` at this step (formatted as astring) | | `invalid` | integer | Number of emails marked as `INVALID` at this step | | `bounce_` | string | Bounce rate percentage for this step (formatted as astring) | | `bounce` | integer | Number of bounced emails at this step | | `sunFrom - satTo` | null | Deprecated. Always `null` | | `monday - sunday` | array[object] | Array of sending time windows per weekday. There can be two sending windows per day | |   └─`[].from` | integer | The start time of an email sending window, measured in minutes from midnight. Minimum `0`, maximum `1440`, `-1` means no emails will be sent this day | |   └─`[].to` | integer |The end time of an email sending window, measured in minutes from midnight. Minimum `0`, maximum `1440`, `-1` means no emails will be sent this day | There is no campaign matching your criteria ``` Status: 204 Body: None ``` Invalid request or malformed request syntax. Please review the [request](#request) ```json { "status": { "status": "ERROR", "code": "E_WRONG_PARAM", "msg": "Wrong param [id]=campaignName" | "Unknown param:ID" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Get campaign Fetch a campaign. Use this endpoint to review your campaign settings, including sending limits, attached mailboxes and LinkedIn accounts; content and settings for each step and version, such as delivery times and tracking options. You can also obtain the step or version IDs for use in update requests. If you are looking for: * Campaign IDs - see `rest/v1/campaign_list` endpoint [here](GET-campaign-list-v1.mdx) * Campaign statistics - see `rest/v1/campaign_list?id={id}` endpoint [here](GET-campaign-stats-v1.mdx) or consider [predefined reports](/docs/reports/reports.md) ## Request ### Endpoint ``` https://api.woodpecker.co/rest/v2/campaigns/{campaign_id} ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Sample request ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getCampaignDetails(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getCampaignDetails(123) # Example campaign ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; // Example campaign ID getCampaignDetails(campaignId); } public static void getCampaignDetails(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getCampaignDetails(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getCampaignDetails(123); // Example campaign ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); $campaignId = '{campaign_id}'; try { $response = $client->get("campaigns/{$campaignId}"); 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 Request processed successfully The below payload represents a simple one-step campaign sent from one email address. The campaign and step settings are the default values where applicable. ```json { "id": 12345678, "name": "One-step email campaign", "status": "DRAFT", "bounce_shield_autopaused_at": null, "email_account_ids": [112233], "settings": { "timezone": "Europe/Warsaw", "prospect_timezone": false, "daily_enroll": 50, "gdpr_unsubscribe": false, "list_unsubscribe": false, "open_disabled_list": [], "auto_pause_prospect_from_domain": false, "auto_pause_prospect_from_domain_statuses": ["REPLIED", "BOUNCED"], "catch_all_verification_mode": "BALANCED", "count_followup_delay_in_working_days": false }, "steps": { "id": "b90637e7-8ccd-4df1-86a6-d07581abbf3e", "type": "START", "followup": { "id": "169486a5-e375-48cd-81a1-01a7f2a1895f", "type": "EMAIL", "delivery_time": { "MONDAY": [{ "from": "08:00", "to": "18:00" }], "TUESDAY": [{ "from": "08:00", "to": "18:00" }], "WEDNESDAY": [{ "from": "08:00", "to": "18:00" }], "THURSDAY": [{ "from": "08:00", "to": "18:00" }], "FRIDAY": [{ "from": "08:00", "to": "18:00" }] }, "body": { "versions": [ { "id": "2af5c091205511bb54acb86b43c59d378a795f67g9a8a73df34601q293e1664f", "version": "A", "subject": "Example subject line", "message": "

Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message.

Best wishes,
Woodpecker team

", "signature": "SENDER", "track_opens": true } ] }, "followup_after": { "range": "DAY", "value": 1 }, "followup": null } } } ``` The below payload represents a two-step linkedin campaign. Note that LinkedIn accounts are attached to a specific step of a campaign, not the whole campaign. The campaign and step settings are the default values where applicable. ```json { "id": 12345678, "name": "Two-step LinkedIn campaign", "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": false, "auto_pause_prospect_from_domain_statuses": ["REPLIED", "BOUNCED"], "catch_all_verification_mode": "BALANCED", "count_followup_delay_in_working_days": false }, "steps": { "id": "b90637e7-8ccd-4df1-86a6-d07581abbf3e", "type": "START", "followup": { "id": "169486a5-e375-48cd-81a1-01a7f2a1895f", "type": "LINKEDIN", "body": { "action_type": "VISIT_PROFILE", "linkedin_account_id": 200002, "versions": [ { "id": "2af5c091205511bb54acb86b43c59d378a795f67g9a8a73df34601q293e1664f", "version": "A", "message": "" } ] }, "followup_after": { "range": "DAY", "value": 3 }, "followup": { "id": "78e2a01a-ebac-1962-ac17-589a862ce3c4", "type": "LINKEDIN", "body": { "action_type": "CONNECTION_REQUEST", "linkedin_account_id": 200002, "versions": [ { "id": "e76ddde4cc34e506a7bf1078e877aece1f04eb5c603a7ac2d5399ea4679e450a", "version": "A", "message": "" }, { "id": "832dca5d7e2ae1106d1f24624b199bdc8080322ac434115495d01ff9ad2c0986", "version": "B", "message": "Hello {{FIRST_NAME}}, I'd like to add you to my professional network on LinkedIn" } ] }, "followup_after": { "range": "DAY", "value": 1 }, "followup": null } } } } ``` The below payload represents a four-step multichannel campaign. The campaign utilizes both LinkedIn and email steps. It also includes custom options such as multiple versions of a step, tracking settings, and several delivery windows throughout the day. ```json { "id": 12345679, "name": "Four step campaign with custom configuration", "status": "PAUSED", "bounce_shield_autopaused_at": "2025-02-10T14:14:57+01:00", "email_account_ids": [100001, 100002, 100003], "settings": { "timezone": "Europe/Warsaw", "prospect_timezone": true, "daily_enroll": 10, "gdpr_unsubscribe": true, "list_unsubscribe": true, "open_disabled_list": ["google.com", "OTHER_PROVIDER"], "auto_pause_prospect_from_domain": true, "auto_pause_prospect_from_domain_statuses": ["REPLIED", "BOUNCED"], "catch_all_verification_mode": "MAXIMUM", "count_followup_delay_in_working_days": true }, "steps": { "id": "e688d52a-b867-4690-acf2-3809286915b1", "type": "START", "followup": { "id": "169486a5-e375-48cd-81a1-01a7f2a1895f", "type": "LINKEDIN", "body": { "action_type": "VISIT_PROFILE", "linkedin_account_id": 200002, "versions": [ { "id": "2af5c091205511bb54acb86b43c59d378a795f67g9a8a73df34601q293e1664f", "version": "A", "message": "" } ] }, "followup_after": { "range": "HOUR", "value": 2 }, "followup": { "id": "36c4fb2c-6f4f-45bf-aeae-4903501193hd", "type": "EMAIL", "delivery_time": { "MONDAY": [{ "from": "09:00", "to": "18:00" }], "TUESDAY": [{ "from": "09:00", "to": "18:00" }], "WEDNESDAY": [{ "from": "09:00", "to": "18:00" }], "THURSDAY": [{ "from": "09:00", "to": "18:00" }] }, "body": { "versions": [ { "id": "2af5c021295511bb54acb87b47c59c378a795f67f9a1b73dd34609c193e1664f", "version": "A", "subject": "Example subject line - version A", "message": "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", "signature": "SENDER", "track_opens": true }, { "id": "c61b5583c7d19fd04d23b2181a17af640c4cf011490acd6f9e4537f62db0e7ba", "version": "B", "subject": "Example subject line - version B", "message": "
{{SPINTAX | \"Hi\" | \"Hello\" | \"Good morning\"}} {{FIRST_NAME}},

Yet another example of a cold email message. 

All the best, 
", "signature": "SENDER", "track_opens": false } ] }, "followup_after": { "range": "DAY", "value": 4 }, "followup": { "id": "78e2a01a-ebac-1962-ac17-589a862ce3c4", "type": "LINKEDIN", "body": { "action_type": "CONNECTION_REQUEST", "linkedin_account_id": 200002, "versions": [ { "id": "e76ddde4cc34e506a7bf1078e877aece1f04eb5c603a7ac2d5399ea4679e450a", "version": "A", "message": "" }, { "id": "832dca5d7e2ae1106d1f24624b199bdc8080322ac434115495d01ff9ad2c0986", "version": "B", "message": "Hello {{FIRST_NAME}}, I'd like to add you to my professional network on LinkedIn" } ] }, "followup_after": { "range": "DAY", "value": 1 }, "followup": { "id": "a99e297f-8423-4600-8c24-5bc21b936302", "type": "EMAIL", "delivery_time": { "THURSDAY": [{ "from": "06:00", "to": "08:00" }, { "from": "13:00", "to": "15:00" }] }, "body": { "versions": [ { "id": "06cb0a70aba1dedae5a9eb949286fb2326f1e9105851a6be6f187e47c131c7be", "version": "A", "subject": null, "message": "
First email followup, version A, sender's signature, no open tracking, same subject line
", "signature": "SENDER", "track_opens": false }, { "id": "4d7d78eebd5abe71eee873b5da40d525e16f0f3611a8a543cae274a5a772a54f", "version": "B", "subject": null, "message": "
First email followup, version B, no signature, no open tracking, same subject line
", "signature": "NO_SIGNATURE", "track_opens": false }, { "id": "4ac2bec5a9082b090aad90eeaa230fe08e7a5ab3a6b968d9328ec437316c9037", "version": "C", "subject": null, "message": "
First email followup, version C, sender's signature, open tracking, same subject line
", "signature": "SENDER", "track_opens": true } ] }, "followup_after": { "range": "DAY", "value": 3 }, "followup": null } } } } } } ``` ### Body schema This section provides the body schema for each object in the campaign payloads. You can also refer to [the campaign schema](campaigns.mdx). Campaign configuration object This object is shared between LinkedIn and email campaigns | Field | Type | Description | |-------|------|-------------| | `id` | integer | Unique identifier of the campaign | | `name` | string | Name of the campaign | | `status` | string | Current campaign status. Possible values: `RUNNING`, `DRAFT`, `STOPPED`, `PAUSED`, `EDITED`, `COMPLETED` | | `bounce_shield_autopaused_at` | string/null | Date and time when [Bounce Shield Monitor](https://woodpecker.co/help-center/en/articles/15228700-bounce-shield-monitor-in-woodpecker-campaigns) automatically paused the campaign after reaching the configured bounce rate threshold. Returned in ISO 8601 format. Returns `null` when the campaign has not been automatically paused | | `email_account_ids` | array[integer] | List of email account SMTP IDs used in this campaign. Use [/mailboxes endpoint](/docs/mailboxes/mailboxes.md) to review them | | `settings` | object | Campaign-level settings like timezone, sending limit, unsubscribe settings, etc | | └─`timezone` | string | 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 | | └─`prospect_timezone` | boolean | Whether to adjust sending times to prospect's timezone instead of the campaign `timezone`. Applies to `EMAIL` steps | | └─`daily_enroll` | integer | Maximum number of prospects that can be contacted in the opening step of the campaign per day. This limit is applied per mailbox or LinkedIn account | | └─`gdpr_unsubscribe` | boolean | Whether the unsubscribe link should provide prospects with an option for [GDPR-compliant data removal](https://woodpecker.co/help-center/en/articles/5258897). This option will work only if the \{\{UNSUBSCRIBE\}\} snippet is included in your email or account signature | | └─`list_unsubscribe` | boolean | Whether to include [List-Unsubscribe header](https://woodpecker.co/help-center/en/articles/5258897). This option will work only if the \{\{UNSUBSCRIBE\}\} snippet is included in your email or account signature | | └─`open_disabled_list` | array[string]/null | 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 | Legacy flag kept for older campaigns. For new API integrations, use `auto_pause_prospect_from_domain_statuses` | | └─`auto_pause_prospect_from_domain_statuses` | array[string] | Prospect statuses that trigger same-domain auto-pause. Allowed values: `REPLIED`, `BOUNCED`. When enabled, if one prospect replies or bounces, Woodpecker will pause other prospects from that domain in a given campaign. Common free domains such as `gmail.com` and `outlook.com` are excluded. When present, this field takes precedence over `auto_pause_prospect_from_domain` | | └─`catch_all_verification_mode` | string | [Catch-all email verification mode](https://woodpecker.co/help-center/en/articles/10233496) - how to approach contacting prospects using catch-all emails. `NONE` - contact all catch-all emails, including undeliverable `BALANCED` - contact deliverable and risky catch-all emails `MAXIMUM` - contact only deliverable catch-all emails `ONLY_VERIFY` - do not contact catch-all emails | | └─`count_followup_delay_in_working_days` | boolean | Whether follow-up delays count only working days. When `true`, Saturdays and Sundays are skipped while the original sending time is preserved. When `false`, weekends count toward the delay. [Learn more](https://woodpecker.co/help-center/en/articles/5258787#h_23ad4abb90) | | `steps` | object | Campaign steps, including all LinkedIn actions or emails and their delivery times, content, etc | Step objects The `START` step must always be the first (root) step of the campaign and cannot occur elsewhere. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `id` | string | - | Unique identifier of the step (UUID) | | `type` | string | - | Use `START` to indicate a start step | | `followup` | object | - | The next step in the sequence. For a `START` step, this field is required and must point to the first `EMAIL` or `LINKEDIN` step sent to prospects | This type defines an email step of a campaign, including its content, versions, delivery times, and follow-ups. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `id` | string | - | Unique identifier of the step (UUID) | | `type` | string | - | Use `EMAIL` to indicate it is an email step | | `delivery_time` | object | - | Time intervals during which emails can be sent. Described in more detail [below](#delivery-time-object) | | `body` | object | - | Email content configuration including A/B test versions. Described in more detail [below](#body-object) | | `followup_after` | object | 1 DAY | Object that specifies the time delay before processing a prospect in the next step; if `delivery_time` allows it. If not provided, a default delay of `1 DAY` will be applied | | └─`range` | string | DAY | Time unit: `DAY`, `HOUR`, `MINUTE` | | └─`value` | integer | 1 | Value of the time unit (range: 1 - 9999) | | `followup` | object/null | null (meaning no followup) | Next step in the sequence. Should consist of an `EMAIL` or `LINKEDIN` step object. Null indicates end of sequence | #### 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](campaigns.mdx#campaign-configuration-object). Each step must define at least one delivery interval. You can assign up to three intervals per day, but they must not overlap. If a day is omitted from the object, no emails will be sent on that day. To specify a whole day interval, you can use either `"from": "00:00", "to": "00:00"` or `"from": "00:00", "to": "24:00"`. The first format is set as the default. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `MONDAY`...`SUNDAY` | array[object] | - | Array of time windows for each day. Maximum 3 windows per day. The valid keys are the days of the week: `MONDAY`, `TUESDAY`, `WEDNESDAY`, `THURSDAY`, `FRIDAY`, `SATURDAY`, `SUNDAY` | | └─`[].from` | string | - | Start time in "HH:mm" format (24-hour) | | └─`[].to` | string | - | End time in "HH:mm" format (24-hour) | #### Body object The `body` and `versions` objects define the email content, A/B 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 | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of email version objects and their definitions. At least one version is required | | └─`[].id` | string | - | Unique identifier of the email version | | └─`[].version` | string | A | Version Identifier. The default version is `A`. Available versions are `A` through `E`. When creating a campaign, the versions are determined by their order in the array, not by explicit declaration | | └─`[].subject` | string/null | - | Email subject line. Required for the first `EMAIL` step. If multiple versions exist, all must include a subject. For later `EMAIL` steps, a null subject sends the message as a follow-up in the same thread. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | └─`[].message` | string | - | Email body content in HTML format. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884). To track individual link clicks ([not recommended](https://woodpecker.co/help-center/en/articles/5267688)), enclose the href attribute value in a \{\{CLICK\}\} snippet. Example: `click here` | | └─`[].signature` | string | NO_SIGNATURE | Whether to use the sender's email account signature. The available options are: `SENDER` or `NO_SIGNATURE` | | └─`[].track_opens` | boolean | false | Whether to track email opens for this email version | This type defines a linkedin step of a campaign, including its action type, content, versions, and follow-ups. You can define the action to perform within the `body`. Available actions: `VISIT_PROFILE`, `CONNECTION_REQUEST`, `DIRECT_MESSAGE`, `INMAIL_MESSAGE` | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `id` | string | - | Unique identifier of the step (UUID) | | `type` | string | - | Use `LINKEDIN` to indicate it is a linkedin step | | `body` | object | - | LinkedIn acton configuration. Described in more detail [below](#body-object-1) | | `followup_after` | object | 1 DAY | Object that specifies the time delay before processing a prospect in the next step. If not provided, a default delay of `1 DAY` will be applied | | └─`range` | string | DAY | Time unit: `DAY`, `HOUR`, `MINUTE` | | └─`value` | integer | 1 | Value of the time unit (range: 1 - 9999) | | `followup` | object/null | null (meaning no followup) | Next step in the sequence. Should consist of an `EMAIL` or `LINKEDIN` step object. Null indicates end of sequence | #### Body object The `body` and `versions` define the LinkedIn action type and its content. All action types share the same data structure but differ in their use of `version`, `subject`, and `message` fields. Supported actions are: `VISIT_PROFILE`, `CONNECTION_REQUEST`, `DIRECT_MESSAGE`, and `INMAIL_MESSAGE`. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of LinkedIn action version objects. Only one boilerplate version will be returned for `VISIT_PROFILE` | | └─`[].id` | string | - | Unique identifier of the LinkedIn action version | | └─`[].version` | string | A | Version Identifier. For `VISIT_PROFILE` value will always be `A` | | └─`[].message` | string | "" | For `VISIT_PROFILE` value will always be an empty string | | `body.linkedin_account_id` | integer | null | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Action type that will be performed in LinkedIn: `VISIT_PROFILE` | | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of LinkedIn action version objects. At least one version is required | | └─`[].id` | string | - | Unique identifier of the LinkedIn action version | | └─`[].version` | string | A | Version Identifier. The default version is `A`. Available versions are `A` through `E`. When creating a campaign, the versions are determined by their order in the array, not by explicit declaration | | └─`[].message` | string | "" | Connection request message content. An empty string (`""`) sends a connection request without a message; otherwise, the provided note will be included with the request. Character limits: `CLASSIC` accounts - 200 characters, `PREMIUM`, `RECRUITER_LITE`, `SALES_NAVIGATOR` - 300 characters. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `body.linkedin_account_id` | integer | null | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Action type that will be performed in LinkedIn: `CONNECTION_REQUEST` | | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of LinkedIn action version objects. At least one version is required | | └─`[].id` | string | - | Unique identifier of the LinkedIn action version | | └─`[].version` | string | A | Version Identifier. The default version is `A`. Available versions are `A` through `E`. When creating a campaign, the versions are determined by their order in the array, not by explicit declaration | | └─`[].message` | string | - | Direct message content. Unlike other actions, message content is required. Character limit: 6000 characters. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `body.linkedin_account_id` | integer | null | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Action type that will be performed in LinkedIn: `DIRECT_MESSAGE` | | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of LinkedIn action version objects. At least one version is required | | └─`[].id` | string | - | Unique identifier of the LinkedIn action version | | └─`[].version` | string | A | Version Identifier. The default version is `A`. Available versions are `A` through `E`. When creating a campaign, the versions are determined by their order in the array, not by explicit declaration | | └─`[].subject` | string/null | null | InMail subject. When present, it cannot contain snippets and must be at most 200 characters | | └─`[].message` | string | - | InMail message content. Message content is required. Character limit: 1900 characters | | `body.linkedin_account_id` | integer | null | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Action type that will be performed in LinkedIn: `INMAIL_MESSAGE` | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx). ```json { "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. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | The requested campaign uses features that are currently supported only via the UI. Please refer to the [campaign configuration](campaigns.mdx). ```json { "code": "API_UNSUPPORTED_CAMPAIGN_FEATURES", "message": "Campaign contains currently unsupported features", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Unexpected error, please try again later. ```json { "code": "UNKNOWN", "message": "Unknown error during get campaign call", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `detail` | string/null | Additional information | --- ## Update step version Use this request to update a step version. * `EMAIL` steps - you can update the subject line, message content, signature settings, and open tracking preferences * `LINKEDIN` steps - you can update the message content for `CONNECTION_REQUEST`, `DIRECT_MESSAGE` and `INMAIL_MESSAGE`; `INMAIL_MESSAGE` versions also support updating the `subject` This endpoint supports partial updates. Only campaigns with a status of `DRAFT` or `EDITED` can be updated. To change the campaign status to `EDITED` use the [/make_editable endpoint](POST-editable-campaign.mdx). ## Request ### Endpoint ``` PATCH https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}/versions/{version_id} ``` You can fetch the `step_id` and `version_id` using the [GET /campaigns structure endpoint](GET-campaign.mdx). ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The request body is a simplified version of the [campaign body object](campaigns.mdx#body-object). You can update each parameter individually without impacting others, as the API supports partial updates. ```json { "subject": "New subject line", "message": "Engaging message", "signature": "SENDER", "track_opens": true } ``` Body schema The `versions` object defines the email content, open tracking, and signature settings. Each of these can be configured individually, as the endpoint supports partial updates. | Field | Type | Required | Description | |-------|------|---------|----| | `subject` | string | No | Email subject line. This field is required to run a campaign if the version is part of the first `EMAIL` step in the sequence. If the step is not the first EMAIL step, you can use an empty string (`""`) to send the message as a follow-up in the same thread. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `message` | string | No | Email body content in HTML format. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884). To track individual link clicks ([not recommended](https://woodpecker.co/help-center/en/articles/5267688)), enclose the href attribute value in a \{\{CLICK\}\} snippet. Example: `click here` | | `signature` | string | No | Whether to use the sender's email account signature. The available options are: `SENDER` or `NO_SIGNATURE` | | `track_opens` | boolean | No | Whether to track email opens for this email version | ### Request samples #### Update step version ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}/versions/{version_id}" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "message": "Engaging message" }' ``` ```Python import requests def updateStepVersionMessage(campaign_id, step_id, version_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}/versions/{version_id}" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "message": "Engaging message" } response = requests.patch(url, headers=headers, json=payload) if response.status_code == 200: print("PATCH successful:", response.json()) else: print("PATCH failed with status:", response.status_code) if __name__ == "__main__": updateStepVersionMessage(123, 456, 789) # Example IDs ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; int stepId = 456; int versionId = 789; updateStepVersionMessage(campaignId, stepId, versionId); } public static void updateStepVersionMessage(int campaignId, int stepId, int versionId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/steps/" + stepId + "/versions/" + versionId; String jsonData = "{" + "\"message\": \"Engaging message\"" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("PATCH response: " + response.body()); } else { System.err.println("PATCH request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function updateStepVersionMessage(campaignId, stepId, versionId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/steps/${stepId}/versions/${versionId}`; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { message: "Engaging message" }; try { const response = await axios.patch(url, data, { headers: headers }); if (response.status === 200) { console.log("PATCH successful:", response.data); } else { console.error("PATCH failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } updateStepVersionMessage(123, 456, 789); // Example IDs ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $campaignId = '{campaign_id}'; $stepId = '{step_id}'; $versionId = '{version_id}'; try { $response = $client->patch("campaigns/{$campaignId}/steps/{$stepId}/versions/{$versionId}", [ 'json' => [ 'message' => 'Engaging message', ], ]); 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 The step version has been updated. ```json { "id": "2a72ca821042bb4baf6f815b8427772a1a13969164c6bcfcab9c0c085994edf9", "version": "A", "subject": "New subject line", "message": "Engaging message", "signature": "SENDER", "track_opens": false } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique ID of the version | | `version` | string | Indicator of A/B version of the updated version | | `subject` | string/null | Current subject line | | `message` | string | Current message | | `signature` | string | Signature setting. The available options are: `SENDER` or `NO_SIGNATURE` | | `track_opens` | boolean | Whether to track email opens for this email version | Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "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 | Error message | | `details` | object/null | Additional information if available| | └─`errors` | array[object] | An array of error objects | |   └─`[].field` | string | Specifies the parameter with an issue | |   └─`[].detail` | string | Description of the issue | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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, step or version doesn't exist. ```json { "code": "NOT_FOUND", "message": "Step version not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Only campaigns with a status of `DRAFT` or `EDITED` can be updated. To change the campaign status to `EDITED` use the [/make_editable endpoint](POST-editable-campaign.mdx). ```json { "code": "NOT_EDITABLE_STATUS" | "API_UNSUPPORTED_CAMPAIGN_FEATURES", "message": "The campaign must be in DRAFT or EDITED status to be updated" | "Campaign contains currently unsupported features", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An unknown error while updating the campaign. Please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during update step version call", "details": null } ``` ```json { "subject": "Quick question about your outbound workflow", "message": "Hi {{FIRST_NAME}}, I noticed your team is growing fast and thought Woodpecker could help streamline outbound follow-ups." } ``` Body schema Note that each LinkedIn action type has different requirements regarding `subject`, message length, and whether a message is mandatory. | Field | Type | Required | Description | |-------|------|---------|----| | `subject` | string/null | `INMAIL_MESSAGE`: no | InMail subject. Supported only for `INMAIL_MESSAGE`. When present, it cannot contain snippets and must be at most 200 characters | | `message` | string | `VISIT_PROFILE`: n/a `CONNECTION_REQUEST`: no`DIRECT_MESSAGE`: yes`INMAIL_MESSAGE`: yes | Content of the message. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884). `CONNECTION_REQUEST`: Empty string (`""`) sends a connection request without a message; otherwise, the provided note will be included with the request. Character limits: `CLASSIC` accounts - 200 characters, `PREMIUM`, `RECRUITER_LITE`, `SALES_NAVIGATOR` - 300 characters. `DIRECT_MESSAGE`: The message is required, character limit - 6000 characters`INMAIL_MESSAGE`: The message is required, character limit - 1900 characters | Request samples #### Update step version ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}/versions/{version_id}" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "subject": "Quick question about your outbound workflow", "message": "Hi {{FIRST_NAME}}, I noticed your team is growing fast and thought Woodpecker could help streamline outbound follow-ups." }' ``` ```Python import requests def updateStepVersionMessage(campaign_id, step_id, version_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}/versions/{version_id}" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "subject": "Quick question about your outbound workflow", "message": "Hi {{FIRST_NAME}}, I noticed your team is growing fast and thought Woodpecker could help streamline outbound follow-ups." } response = requests.patch(url, headers=headers, json=payload) if response.status_code == 200: print("PATCH successful:", response.json()) else: print("PATCH failed with status:", response.status_code) if __name__ == "__main__": updateStepVersionMessage(123, 456, 789) # Example IDs ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; int stepId = 456; int versionId = 789; updateStepVersionMessage(campaignId, stepId, versionId); } public static void updateStepVersionMessage(int campaignId, int stepId, int versionId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/steps/" + stepId + "/versions/" + versionId; String jsonData = "{" + "\"subject\": \"Quick question about your outbound workflow\"," + "\"message\": \"Hi {{FIRST_NAME}}, I noticed your team is growing fast and thought Woodpecker could help streamline outbound follow-ups.\"" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("PATCH response: " + response.body()); } else { System.err.println("PATCH request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function updateStepVersionMessage(campaignId, stepId, versionId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/steps/${stepId}/versions/${versionId}`; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { subject: "Quick question about your outbound workflow", message: "Hi {{FIRST_NAME}}, I noticed your team is growing fast and thought Woodpecker could help streamline outbound follow-ups." }; try { const response = await axios.patch(url, data, { headers: headers }); if (response.status === 200) { console.log("PATCH successful:", response.data); } else { console.error("PATCH failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } updateStepVersionMessage(123, 456, 789); // Example IDs ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $campaignId = '{campaign_id}'; $stepId = '{step_id}'; $versionId = '{version_id}'; try { $response = $client->patch("campaigns/{$campaignId}/steps/{$stepId}/versions/{$versionId}", [ 'json' => [ 'subject' => 'Quick question about your outbound workflow', 'message' => 'Hi {{FIRST_NAME}}, I noticed your team is growing fast and thought Woodpecker could help streamline outbound follow-ups.', ], ]); 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 The step version has been updated. ```json { "id": "2a72ca821042bb4baf6f815b8427772a1a13969164c6bcfcab9c0c085994edf9", "version": "A", "subject": "Quick question about your outbound workflow", "message": "Hi {{FIRST_NAME}}, I noticed your team is growing fast and thought Woodpecker could help streamline outbound follow-ups." } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `id` | string | Unique ID of the version | | `version` | string | Indicator of A/B version of the updated version | | `subject` | string/null | Current LinkedIn subject. Used for `INMAIL_MESSAGE`; other LinkedIn actions may return `null` | | `message` | string | Current message | Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "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 | Error message | | `details` | object/null | Additional information if available | | └─`errors` | array[object] | An array of error objects | |   └─`[].field` | string | Specifies the parameter with an issue | |   └─`[].detail` | string | Description of the issue | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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, step or version doesn't exist. ```json { "code": "NOT_FOUND", "message": "Step version not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Only campaigns with a status of `DRAFT` or `EDITED` can be updated. To change the campaign status to `EDITED` use the [/make_editable endpoint](POST-editable-campaign.mdx). ```json { "code": "NOT_EDITABLE_STATUS" | "API_UNSUPPORTED_CAMPAIGN_FEATURES", "message": "The campaign must be in DRAFT or EDITED status to be updated" | "Campaign contains currently unsupported features", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An unknown error while updating the campaign. Please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during update step version call", "details": null } ``` --- ## Update step Use this request to update an `EMAIL` step's delivery times. This request replaces the entire `delivery_times` object. Any existing values not included in the request will be overwritten. Delivery hours for `LINKEDIN` steps cannot be edited. Only campaigns with a status of `DRAFT` or `EDITED` can be updated. To change the campaign status to `EDITED` use the [/make_editable endpoint](POST-editable-campaign.mdx). ## Request ### Endpoint ``` PATCH https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id} ``` You can fetch the `step_id` and `version_id` using the [GET /campaigns structure endpoint](GET-campaign.mdx). ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The request body is the [delivery time object](campaigns.mdx#delivery-time-object). Below is a simplified example that would overwrite the current settings and configure the campaign to be sent on two specific days. ```json { "delivery_time": { "WEDNESDAY": [ { "from": "09:00", "to": "17:00" } ], "THURSDAY": [ { "from": "09:00", "to": "11:00" }, { "from": "14:00", "to": "16:00" } ] } } ``` #### Body schema The `delivery_time` object defines the time intervals during which email messages can be sent. **You can assign up to three time intervals to a single day**. 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 3 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) | ### Request samples #### Update step delivery times ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}" \ --header "Content-Type: application/json" \ --header "x-api-key: {YOUR_API_KEY}" \ --data '{ "delivery_time": { "WEDNESDAY": [ { "from": "09:00", "to": "17:00" } ], "THURSDAY": [ { "from": "09:00", "to": "11:00" }, { "from": "14:00", "to": "16:00" } ] } }' ``` ```Python import requests def updateStepDeliveryTimes(campaign_id, step_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps/{step_id}" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "delivery_time": { "WEDNESDAY": [ { "from": "09:00", "to": "17:00" } ], "THURSDAY": [ { "from": "09:00", "to": "11:00" }, { "from": "14:00", "to": "16:00" } ] } } response = requests.patch(url, headers=headers, json=payload) if response.status_code == 200: print("PATCH successful:", response.json()) else: print("PATCH failed with status:", response.status_code) if __name__ == "__main__": updateStepDeliveryTimes(123, 456) # Example IDs ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; int stepId = 456; updateStepDeliveryTimes(campaignId, stepId); } public static void updateStepDeliveryTimes(int campaignId, int stepId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/steps/" + stepId; String jsonData = "{" + "\"delivery_time\": {" + "\"WEDNESDAY\": [" + " {\"from\": \"09:00\", \"to\": \"17:00\"}" + "]," + "\"THURSDAY\": [" + " {\"from\": \"09:00\", \"to\": \"11:00\"}," + " {\"from\": \"14:00\", \"to\": \"16:00\"}" + "]" + "}" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("PATCH response: " + response.body()); } else { System.err.println("PATCH request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function updateStepDeliveryTimes(campaignId, stepId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/steps/${stepId}`; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { delivery_time: { WEDNESDAY: [ { from: "09:00", to: "17:00" } ], THURSDAY: [ { from: "09:00", to: "11:00" }, { from: "14:00", to: "16:00" } ] } }; try { const response = await axios.patch(url, data, { headers: headers }); if (response.status === 200) { console.log("PATCH successful:", response.data); } else { console.error("PATCH failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } updateStepDeliveryTimes(123, 456); // Example IDs ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $campaignId = '{campaign_id}'; $stepId = '{step_id}'; try { $response = $client->patch("campaigns/{$campaignId}/steps/{$stepId}", [ 'json' => [ 'delivery_time' => [ 'WEDNESDAY' => [ ['from' => '09:00', 'to' => '17:00'] ], 'THURSDAY' => [ ['from' => '09:00', 'to' => '11:00'], ['from' => '14:00', 'to' => '16: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 The campaign has been updated. A [full campaign payload](campaigns.mdx#campaign-body-schema) will be returned. Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "code": "INPUT_DATA_VALIDATION_FAILURE" | "API_UNSUPPORTED_CAMPAIGN_FEATURES" | "Bad Request", "message": "Input data validation failure" | "Campaign contains currently unsupported features" | "Value of {field_name} is incorrect.", "details": "String" } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 | Campaign not found. Review the ID. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Returned when the step is either not found or is in a state that doesn't allow editing. ```json { "code": "NOT_EDITABLE_STATUS" | "BRANCH_NOT_EDITABLE" | "BRANCH_NOT_FOUND", "message": "The campaign must be in DRAFT or EDITED status to be updated" | "Step is not allowed to be edited" | "Step not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An unknown error while updating the campaign. Please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during campaign call", "details": null } ``` --- ## Update campaign settings Use this request to update campaign-wide settings such as assigned email accounts, sending limits, timezone, and more. The endpoint allows for partial updates. Only campaigns with a status of `DRAFT` or `EDITED` can be updated. To change the campaign status to `EDITED` use the [/make_editable endpoint](POST-editable-campaign.mdx). ## Request ### Endpoint ``` PATCH https://api.woodpecker.co/rest/v2/campaigns/{campaign_id} ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The request body uses the [campaign configuration payload](campaigns.mdx#campaign-configuration-object). You can update each setting individually without impacting others, as the API supports partial updates. ```json { "name":"Updated name", "email_account_ids": [100001, 100002, 100003], "settings": { "timezone":"Pacific/Pago_Pago", "prospect_timezone": true, "daily_enroll": 30, "gdpr_unsubscribe": true, "list_unsubscribe": true, "open_disabled_list": ["google.com", "OTHER_PROVIDER"], "auto_pause_prospect_from_domain_statuses": ["REPLIED", "BOUNCED"], "catch_all_verification_mode": "MAXIMUM", "count_followup_delay_in_working_days": true } } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `name` | string | Name of the campaign | | `email_account_ids` | array[integer] | List of email account SMTP IDs used in this campaign. Use [/mailboxes endpoint](/docs/mailboxes/mailboxes.md) to review them. Chosen mailboxes must be connected to Woodpecker without issues | | `settings` | object |Campaign-level settings object | | └─`timezone` | string |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](campaigns.mdx#campaign-configuration-object) | | └─`prospect_timezone` | boolean | Whether to adjust sending times to prospect's timezone instead of the campaign `timezone` | | └─`daily_enroll` | integer | Maximum number of prospects that can be contacted in the opening step of the campaign per day. This limit is applied per mailbox or LinkedIn account. The default maximum value is 500 | | └─`gdpr_unsubscribe` | boolean | Whether the unsubscribe link should provide prospects with an option for [GDPR-compliant data removal](https://woodpecker.co/help-center/en/articles/5258897). This option will work only if the \{\{UNSUBSCRIBE\}\} snippet is included in your email or account signature | | └─`list_unsubscribe` | boolean | Whether to include [List-Unsubscribe header](https://woodpecker.co/help-center/en/articles/5258897). This option will work only if the \{\{UNSUBSCRIBE\}\} snippet is included in your email or account signature | | └─`open_disabled_list` | array[string] | 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_statuses` | array[string] | Prospect statuses that trigger same-domain auto-pause. Allowed values: `REPLIED`, `BOUNCED`. When enabled, if one prospect replies or bounces, Woodpecker will pause other prospects from that domain in a given campaign. Common free domains such as `gmail.com` and `outlook.com` are excluded | | └─`catch_all_verification_mode` | string | [Catch-all email verification mode](https://woodpecker.co/help-center/en/articles/10233496) - how to approach contacting prospects using catch-all emails. `NONE` - contact all catch-all emails, including undeliverable `BALANCED` - contact deliverable and risky catch-all emails `MAXIMUM` - contact only deliverable catch-all emails `ONLY_VERIFY` - do not contact catch-all emails | | └─`count_followup_delay_in_working_days` | boolean | Whether follow-up delays count only working days. When `true`, Saturdays and Sundays are skipped while the original sending time is preserved. When `false`, weekends count toward the delay. [Learn more](https://woodpecker.co/help-center/en/articles/5258787#h_23ad4abb90) | ### Request samples #### Edit campaign's daily limit ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "settings": { "daily_enroll": 30 } }' ``` ```Python import requests def updateCampaignSettings(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "settings": { "daily_enroll": 30 } } response = requests.patch(url, headers=headers, json=payload) if response.status_code == 200: print("PATCH successful:", response.json()) else: print("PATCH failed with status:", response.status_code) if __name__ == "__main__": updateCampaignSettings(123) # Example campaign ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; // Example campaign ID updateCampaignSettings(campaignId); } public static void updateCampaignSettings(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId; String jsonData = "{" + "\"settings\": {" + "\"daily_enroll\": 30" + "}" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("PATCH response: " + response.body()); } else { System.err.println("PATCH request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function updateCampaignSettings(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}`; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { settings: { daily_enroll: 30 } }; try { const response = await axios.patch(url, data, { headers: headers }); if (response.status === 200) { console.log("PATCH successful:", response.data); } else { console.error("PATCH failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } updateCampaignSettings(123); // Example campaign ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $campaignId = '{campaign_id}'; try { $response = $client->patch("campaigns/{$campaignId}", [ 'json' => [ 'settings' => [ 'daily_enroll' => 30, ], ], ]); 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 The campaign has been updated. A [full campaign payload](campaigns.mdx#campaign-body-schema) will be returned. Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "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 | Error message | | `details` | object / null | Additional information if available | | └─`errors` | array[object] | An array of error objects | |   └─`[].field` | string | Specifies the parameter with an issue | |   └─`[].detail` | string | Description of the issue | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Only campaigns with a status of `DRAFT` or `EDITED` can be updated. To change the campaign status to `EDITED` use the [/make_editable endpoint](POST-editable-campaign.mdx). ```json { "code": "NOT_EDITABLE_STATUS" | "API_UNSUPPORTED_CAMPAIGN_FEATURES", "message": "The campaign must be in DRAFT or EDITED status to be updated" | "Campaign contains currently unsupported features", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Unexpected error, please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during update campaign call", "details": null } ``` --- ## Add campaign step This endpoint allows you to add a new step at the end of a campaign, as long as 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](POST-editable-campaign.mdx). ## 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](/docs/getting-started/authentication.mdx). ### 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](GET-campaign.mdx) ```json { "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": "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", "signature": "SENDER", "track_opens": true }, { "subject": "Example subject line - version B", "message": "
{{SPINTAX | \"Hi\" | \"Hello\" | \"Good morning\"}} {{FIRST_NAME}},

Yet another example of a cold email message. 

All the best, 
", "signature": "NO_SIGNATURE", "track_opens": false } ] } } } ``` ```json { "parent_id": "9e8e8ebb-c6e1-4b8f-be37-a5542513dbge", "step": { "type": "LINKEDIN", "body": { "action_type": "CONNECTION_REQUEST", "linkedin_account_id": 200002, "versions": [ { "message": "" }, { "message": "Hello {{FIRST_NAME}}, I'd like to add you to my professional network on LinkedIn" } ] } } } ``` #### 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](campaigns.mdx). This type defines an email step of a campaign, including its content, versions and delivery times. | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `parent_id` | string | - | Yes | ID of the preceding campaign step. The new step will be appended directly after this step in the campaign flow. The parent step must not have processed any prospects and be the last step of the campaign | | `type` | string | - | Yes | Use `EMAIL` to indicate it is an email step | | `delivery_time` | object | - | Yes | Time intervals during which emails can be sent. Described in more detail [below](#delivery-time-object) | | `body` | object | - | Yes | Email content configuration including A/B test versions. Described in more detail [below](#body-object) | | `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. If not provided, a default delay of `1 DAY` will be applied | | └─`range` | string | DAY | No | Time unit: `DAY`, `HOUR`, `MINUTE` | | └─`value` | integer | 1 | No | Value of the time unit (range: 1 - 9999) | #### 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](campaigns.mdx#campaign-configuration-object). Each step must define at least one delivery interval. You can assign up to three intervals per day, but they must not overlap. If a day is omitted from the object, no emails will be sent on that day. To specify a whole day interval, you can use either `"from": "00:00", "to": "00:00"` or `"from": "00:00", "to": "24:00"`. The first format is set as the default. | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `MONDAY`...`SUNDAY` | array[object] | - | Yes | Array of time windows for each day. Maximum 3 windows per day. 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 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 | | └─`[].subject` | string/null | - | Yes, for the first `EMAIL` step | Email subject line. Required for the first `EMAIL` step in a campaign. If multiple versions exist, all must include a subject. For later `EMAIL` steps, a null subject sends the message as a follow-up in the same thread. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | └─`[].message` | string | - | Yes | Email body content in HTML format. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884). To track individual link clicks ([not recommended](https://woodpecker.co/help-center/en/articles/5267688)), enclose the href attribute value in a \{\{CLICK\}\} snippet. Example: `click here` | | └─`[].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 | false | No | Whether to track email opens for this email version | This type defines a linkedin step of a campaign, including its action type, content and versions. You can define the action to perform within the `body`. Available actions: `VISIT_PROFILE`, `CONNECTION_REQUEST`, `DIRECT_MESSAGE`, `INMAIL_MESSAGE` | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `parent_id` | string | - | Yes | ID of the preceding campaign step. The new step will be appended directly after this step in the campaign flow. The parent step must not have processed any prospects and be the last step of the campaign | | `type` | string | - | Yes | Use `LINKEDIN` to indicate it is a linkedin step | | `body` | object | - | Yes | LinkedIn acton configuration. Described in more detail [below](#body-object-1) | | `followup_after` | object | 1 DAY | No | Object that specifies the time delay before processing a prospect in the next step. If not provided, a default delay of `1 DAY` will be applied | | └─`range` | string | DAY | No | Time unit: `DAY`, `HOUR`, `MINUTE` | | └─`value` | integer | 1 | No | Value of the time unit (range: 1 - 9999) | #### Body object The `body` and `versions` define the LinkedIn action type and its content. Supported actions are: `VISIT_PROFILE`, `CONNECTION_REQUEST`, `DIRECT_MESSAGE`, and `INMAIL_MESSAGE`. | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `body.linkedin_account_id` | integer | - | Yes | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Yes | Action type that will be performed in LinkedIn: `VISIT_PROFILE` | | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `body.versions` | array[object] | - | Yes | Array of LinkedIn action version objects. At least one version is required | | └─`[].message` | string | null | No | Connection request message content. An empty string (`""`) and null sends a connection request without a message; otherwise, the provided note will be included with the request. `CLASSIC` accounts - 200 characters, `PREMIUM`, `RECRUITER_LITE`, `SALES_NAVIGATOR` - 300 characters. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `body.linkedin_account_id` | integer | - | Yes | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Yes | Action type that will be performed in LinkedIn: `CONNECTION_REQUEST` | | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `body.versions` | array[object] | - | Yes | Array of LinkedIn action version objects. At least one version is required | | └─`[].message` | string | - | Yes | Direct message content. Unlike other actions, message content is required. Character limit: 6000 characters. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `body.linkedin_account_id` | integer | - | Yes | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Yes | Action type that will be performed in LinkedIn: `DIRECT_MESSAGE` | | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `body.versions` | array[object] | - | Yes | Array of LinkedIn action version objects. At least one version is required | | └─`[].subject` | string/null | null | No | InMail subject. When present, it cannot contain snippets and must be at most 200 characters | | └─`[].message` | string | - | Yes | InMail message content. Message content is required. Character limit: 1900 characters | | `body.linkedin_account_id` | integer | - | Yes | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Yes | Action type that will be performed in LinkedIn: `INMAIL_MESSAGE` | ### Request samples #### Add step ```bash 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": "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", "signature": "SENDER", "track_opens": true } ] }, "followup_after": { "range": "HOUR", "value": 10 } } }' ``` ```Python import requests def createCampaignStep(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/steps" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "parent_id": "9e8e8ebb-c6e1-4b8f-be37-a5542513dbge", "step": { "type": "EMAIL", "delivery_time": { "TUESDAY": [ { "from": "09:00", "to": "18:00" } ] }, "body": { "versions": [ { "subject": None, "message": "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", "signature": "SENDER", "track_opens": True } ] }, "followup_after": { "range": "HOUR", "value": 10 } } } response = requests.post(url, headers=headers, json=payload) if response.status_code == 201: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": createCampaignStep(123) # Example campaign ID ``` ```java 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 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 createRequestBody() { Map requestBody = new HashMap<>(); requestBody.put("parent_id", "9e8e8ebb-c6e1-4b8f-be37-a5542513dbge"); // Step object Map step = new HashMap<>(); step.put("type", "EMAIL"); // Delivery time Map>> deliveryTime = new HashMap<>(); deliveryTime.put("TUESDAY", Collections.singletonList(Map.of("from", "09:00", "to", "18:00"))); step.put("delivery_time", deliveryTime); // Email body Map emailBody = new HashMap<>(); Map version = new HashMap<>(); version.put("subject", null); version.put("message", "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
"); version.put("signature", "SENDER"); version.put("track_opens", true); emailBody.put("versions", Collections.singletonList(version)); step.put("body", emailBody); // Follow-up after Map 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(); } } ``` ```js const axios = require("axios"); async function createCampaignStep(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/steps`; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const 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: "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", signature: "SENDER", track_opens: true } ] }, followup_after: { range: "HOUR", value: 10 } } }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 201) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } createCampaignStep(123); // Example campaign ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $campaignId = '{campaign_id}'; try { $response = $client->post("campaigns/{$campaignId}/steps", [ 'json' => [ 'parent_id' => '9e8e8ebb-c6e1-4b8f-be37-a5542513dbge', 'step' => [ 'type' => 'EMAIL', 'delivery_time' => [ 'TUESDAY' => [ ['from' => '09:00', 'to' => '18:00'] ] ], 'body' => [ 'versions' => [ [ 'subject' => null, 'message' => '
Hi {{FIRST_NAME | "there"}},

This is an example cold email message. 

Best wishes, 
', 'signature' => 'SENDER', 'track_opens' => true ] ] ], 'followup_after' => [ 'range' => 'HOUR', 'value' => 10 ] ] ], ]); 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 The step has been added. A [full campaign payload](campaigns.mdx#campaign-body-schema) will be returned. Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "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 | Error message | | `details` | object/null | Additional information if available | | └─`errors` | array[object] | An array of error objects | |   └─`[].field` | string | Specifies the parameter with an issue | |   └─`[].detail` | string | Description of the issue | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx). ```json { "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. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | 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. ```json { "code": "PARENT_BRANCH_NOT_LINKABLE" | "NOT_EDITABLE_STATUS" | "API_UNSUPPORTED_CAMPAIGN_FEATURES", "message": "Can not link new steps to this parent" | "The campaign must be in DRAFT or EDITED status to be updated" | "Campaign contains currently unsupported features", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Unexpected error, please try again later. ```json { "code": "UNKNOWN", "message": "Unknown error during add step call", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | --- ## Create campaign Create a **multichannel, LinkedIn or email campaign** with a sequence of personalized follow-up messages and customizable delivery settings. Configure campaign-wide settings like timezone and daily limits, define message 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](/docs/getting-started/authentication.mdx). ### 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. Each campaign must begin with a `START` step, followed by 1 to 16 `EMAIL` or `LINKEDIN` 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. For campaigns with `EMAIL` steps, replace `123456` with the `id` of a connected SMTP mailbox returned by the [/mailboxes endpoint](/docs/mailboxes/mailboxes.md). The below payload represents a simple one-step campaign sent from one email address. ```json { "name": "One-step email campaign", "email_account_ids": [123456], "settings": { "timezone": "Europe/Warsaw", "prospect_timezone": true, "daily_enroll": 25, "gdpr_unsubscribe": true, "list_unsubscribe": false, "open_disabled_list": ["google.com"], "auto_pause_prospect_from_domain_statuses": ["REPLIED", "BOUNCED"], "catch_all_verification_mode": "BALANCED", "count_followup_delay_in_working_days": false }, "steps": { "type": "START", "followup": { "type": "EMAIL", "delivery_time": { "MONDAY": [{ "from": "08:00", "to": "18:00" }], "TUESDAY": [{ "from": "08:00", "to": "18:00" }], "WEDNESDAY": [{ "from": "08:00", "to": "18:00" }], "THURSDAY": [{ "from": "08:00", "to": "18:00" }], "FRIDAY": [{ "from": "08:00", "to": "18:00" }] }, "body": { "versions": [ { "subject": "Example subject line", "message": "

Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message.

Best wishes,
Woodpecker team

", "signature": "SENDER", "track_opens": false } ] } } } } ``` The below payload represents a two-step linkedin campaign. Note that LinkedIn accounts are attached to a specific step of a campaign, not the whole campaign. ```json { "name": "Two-step LinkedIn campaign", "email_account_ids": [], "settings": { "timezone": "Europe/Warsaw", "daily_enroll": 50 }, "steps": { "type": "START", "followup": { "type": "LINKEDIN", "body": { "action_type": "VISIT_PROFILE", "linkedin_account_id": 200002 }, "followup_after": { "range": "DAY", "value": 3 }, "followup": { "type": "LINKEDIN", "body": { "action_type": "CONNECTION_REQUEST", "linkedin_account_id": 200002, "versions": [ { "message": "" }, { "message": "Hello {{FIRST_NAME}}, I'd like to add you to my professional network on LinkedIn" } ] } } } } } ``` The below payload represents a four-step multichannel campaign. The campaign utilizes both LinkedIn and email steps. It also includes custom options such as multiple versions of a step, tracking settings, and several delivery windows throughout the day. ```json { "name": "Four step campaign with custom configuration", "email_account_ids": [123456, 123457, 123458], "settings": { "timezone": "Europe/Warsaw", "prospect_timezone": true, "daily_enroll": 10, "gdpr_unsubscribe": true, "list_unsubscribe": true, "open_disabled_list": ["google.com", "OTHER_PROVIDER"], "auto_pause_prospect_from_domain_statuses": ["REPLIED", "BOUNCED"], "catch_all_verification_mode": "MAXIMUM", "count_followup_delay_in_working_days": true }, "steps": { "type": "START", "followup": { "type": "LINKEDIN", "body": { "action_type": "VISIT_PROFILE", "linkedin_account_id": 200002 }, "followup_after": { "range": "HOUR", "value": 2 }, "followup": { "type": "EMAIL", "delivery_time": { "MONDAY": [{ "from": "09:00", "to": "18:00" }], "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": "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", "signature": "SENDER", "track_opens": true }, { "subject": "Example subject line - version B", "message": "
{{SPINTAX | \"Hi\" | \"Hello\" | \"Good morning\"}} {{FIRST_NAME}},

Yet another example of a cold email message. 

All the best, 
", "signature": "SENDER", "track_opens": false } ] }, "followup_after": { "range": "DAY", "value": 4 }, "followup": { "type": "LINKEDIN", "body": { "action_type": "CONNECTION_REQUEST", "linkedin_account_id": 200002, "versions": [ { "message": "" }, { "message": "Hello {{FIRST_NAME}}, I'd like to add you to my professional network on LinkedIn" } ] }, "followup_after": { "range": "DAY", "value": 1 }, "followup": { "type": "EMAIL", "delivery_time": { "THURSDAY": [{ "from": "06:00", "to": "08:00" }, { "from": "13:00", "to": "15:00" }] }, "body": { "versions": [ { "subject": null, "message": "
First email followup, version A, sender's signature, no open tracking, same subject line
", "signature": "SENDER", "track_opens": false }, { "subject": null, "message": "
First email followup, version B, no signature, no open tracking, same subject line
", "signature": "NO_SIGNATURE", "track_opens": false }, { "subject": null, "message": "
First email followup, version C, sender's signature, open tracking, same subject line
", "signature": "SENDER", "track_opens": true } ] }, "followup_after": { "range": "DAY", "value": 3 }, "followup": null } } } } } } ``` 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](campaigns.mdx). 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 - if campaign contains an `EMAIL` step | List of SMTP mailbox IDs used in this campaign. Use the `id` of a mailbox with `type: "SMTP"` returned by the [/mailboxes endpoint](/docs/mailboxes/mailboxes.md). 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](campaigns.mdx#campaign-configuration-object) | | └─`prospect_timezone` | boolean | false | No | Whether to adjust sending times to prospect's timezone instead of the campaign `timezone`. Applies to `EMAIL` steps | | └─`daily_enroll` | integer | - | Yes | Maximum number of prospects that can be contacted in the opening step of the campaign per day. This limit is applied per mailbox or LinkedIn account. 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](https://woodpecker.co/help-center/en/articles/5258897). 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](https://woodpecker.co/help-center/en/articles/5258897). 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_statuses` | array[string] | [] | No | Prospect statuses that trigger same-domain auto-pause. Allowed values: `REPLIED`, `BOUNCED`. When enabled, if one prospect replies or bounces, Woodpecker will pause other prospects from that domain in a given campaign. Common free domains such as `gmail.com` and `outlook.com` are excluded | | └─`catch_all_verification_mode` | string | `BALANCED` | No | [Catch-all email verification mode](https://woodpecker.co/help-center/en/articles/10233496) - how to approach contacting prospects using catch-all emails. `NONE` - contact all catch-all emails, including undeliverable `BALANCED` - contact deliverable and risky catch-all emails `MAXIMUM` - contact only deliverable catch-all emails `ONLY_VERIFY` - do not contact catch-all emails | | └─`count_followup_delay_in_working_days` | boolean | false | No | Whether follow-up delays count only working days. When `true`, Saturdays and Sundays are skipped while the original sending time is preserved. When `false`, weekends count toward the delay. [Learn more](https://woodpecker.co/help-center/en/articles/5258787#h_23ad4abb90) | | `steps` | object | - | Yes | Campaign steps, including all LinkedIn actions or emails and their delivery times, content, etc | Step objects The `START` step must always be the first (root) step of the campaign and cannot occur elsewhere. | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `type` | string | - | Yes | Use `START` to indicate a start step | | `followup` | object | - | Yes | The next step in the sequence. For a `START` step, this field is required and must point to the first `EMAIL` or `LINKEDIN` step sent to prospects | This type defines an email step of a campaign, including its content, versions, delivery times, and follow-ups. | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `type` | string | - | Yes | Use `EMAIL` to indicate it is an email step | | `delivery_time` | object | - | Yes | Time intervals during which emails can be sent. Described in more detail [below](#delivery-time-object) | | `body` | object | - | Yes | Email content configuration including A/B test versions. Described in more detail [below](#body-object) | | `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. If not provided, a default delay of `1 DAY` will be applied | | └─`range` | string | DAY | No | Time unit: `DAY`, `HOUR`, `MINUTE` | | └─`value` | integer | 1 | No | Value of the time unit (range: 1 - 9999) | | `followup` | object/null | null (meaning no followup) | No | Next step in the sequence. Should consist of an `EMAIL` or `LINKEDIN` step object. Null indicates end of sequence | #### 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](campaigns.mdx#campaign-configuration-object). Each step must define at least one delivery interval. You can assign up to three intervals per day, but they must not overlap. If a day is omitted from the object, no emails will be sent on that day. To specify a whole day interval, you can use either `"from": "00:00", "to": "00:00"` or `"from": "00:00", "to": "24:00"`. The first format is set as the default. | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `MONDAY`...`SUNDAY` | array[object] | - | Yes | Array of time windows for each day. Maximum 3 windows per day. 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 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 | | └─`[].subject` | string/null | - | Yes, for the first `EMAIL` step | Email subject line. Required for the first `EMAIL` step. If multiple versions exist, all must include a subject. For later `EMAIL` steps, a null subject sends the message as a follow-up in the same thread. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | └─`[].message` | string | - | Yes | Email body content in HTML format. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884). To track individual link clicks ([not recommended](https://woodpecker.co/help-center/en/articles/5267688)), enclose the href attribute value in a \{\{CLICK\}\} snippet. Example: `click here` | | └─`[].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 | false | No | Whether to track email opens for this email version | This type defines a linkedin step of a campaign, including its action type, content, versions, and follow-ups. You can define the action to perform within the `body`. Available actions: `VISIT_PROFILE`, `CONNECTION_REQUEST`, `DIRECT_MESSAGE`, `INMAIL_MESSAGE` | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `type` | string | - | Yes | Use `LINKEDIN` to indicate it is a linkedin step | | `body` | object | - | Yes | LinkedIn acton configuration. Described in more detail [below](#body-object-1) | | `followup_after` | object | 1 DAY | No | Object that specifies the time delay before processing a prospect in the next step. If not provided, a default delay of `1 DAY` will be applied | | └─`range` | string | DAY | No | Time unit: `DAY`, `HOUR`, `MINUTE` | | └─`value` | integer | 1 | No | Value of the time unit (range: 1 - 9999) | | `followup` | object/null | null (meaning no followup) | No | Next step in the sequence. Should consist of an `EMAIL` or `LINKEDIN` step object. Null indicates end of sequence | #### Body object The `body` and `versions` define the LinkedIn action type and its content. The data structure remains similar between actions but differs in their use of `version`, `subject`, and `message` fields. Supported actions are: `VISIT_PROFILE`, `CONNECTION_REQUEST`, `DIRECT_MESSAGE`, and `INMAIL_MESSAGE`. | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `body.linkedin_account_id` | integer | - | Yes | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Yes | Action type that will be performed in LinkedIn: `VISIT_PROFILE` | | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `body.versions` | array[object] | - | Yes | Array of LinkedIn action version objects. At least one version is required | | └─`[].message` | string | null | No | Connection request message content. An empty string (`""`) and null sends a connection request without a message; otherwise, the provided note will be included with the request. Character limits: `CLASSIC` accounts - 200 characters, `PREMIUM`, `RECRUITER_LITE`, `SALES_NAVIGATOR` - 300 characters. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `body.linkedin_account_id` | integer | - | Yes | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Yes | Action type that will be performed in LinkedIn: `CONNECTION_REQUEST` | | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `body.versions` | array[object] | - | Yes | Array of LinkedIn action version objects. At least one version is required | | └─`[].message` | string | - | Yes | Direct message content. Unlike other actions, message content is required. Character limit: 6000 characters. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `body.linkedin_account_id` | integer | - | Yes | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Yes | Action type that will be performed in LinkedIn: `DIRECT_MESSAGE` | | Field | Type | Default | Required | Description | |-------|:------:|:---------:|:----------:|-------------| | `body.versions` | array[object] | - | Yes | Array of LinkedIn action version objects. At least one version is required | | └─`[].subject` | string/null | null | No | InMail subject. When present, it cannot contain snippets and must be at most 200 characters | | └─`[].message` | string | - | Yes | InMail message content. Message content is required. Character limit: 1900 characters | | `body.linkedin_account_id` | integer | - | Yes | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Yes | Action type that will be performed in LinkedIn: `INMAIL_MESSAGE` | ### Request samples #### Create campaign ```bash 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": [123456], "settings": { "timezone": "Europe/Warsaw", "daily_enroll": 30 }, "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": "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", "track_opens": true } ] } } } }' ``` ```Python import requests def createCampaign(): url = "https://api.woodpecker.co/rest/v2/campaigns" headers = { "x-api-key": "{YOUR_API_KEY}", # Replace the API key "Content-Type": "application/json" } payload = { "email_account_ids": [123456], # Replace the SMTP mailbox id "settings": { "timezone": "Europe/Warsaw", "daily_enroll": 30 }, "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": "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", "track_opens": True } ] } } } } response = requests.post(url, headers=headers, json=payload) if response.status_code == 201: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": createCampaign() ``` ```java 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 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 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(123456); // Replace the SMTP mailbox id ObjectNode settings = payload.putObject("settings"); settings.put("timezone", "Europe/Warsaw"); settings.put("daily_enroll", 30); 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", "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
"); version.put("track_opens", true); return payload; } } ``` ```js const axios = require("axios"); async function createCampaign() { const url = "https://api.woodpecker.co/rest/v2/campaigns"; const headers = { "x-api-key": "{YOUR_API_KEY}", // Replace the API key "Content-Type": "application/json" }; const data = { email_account_ids: [123456], // Replace the SMTP mailbox id settings: { timezone: "Europe/Warsaw", daily_enroll: 30 }, 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: "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", track_opens: true } ] } } } }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 201) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } createCampaign(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), // Replace the API key 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('campaigns', [ 'json' => [ 'email_account_ids' => [123456], // Replace the SMTP mailbox id 'settings' => [ 'timezone' => 'Europe/Warsaw', 'daily_enroll' => 30, ], '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' => '
Hi {{FIRST_NAME | "there"}},

This is an example cold email message. 

Best wishes, 
', 'track_opens' => true, ] ] ] ] ] ], ]); 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 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 version * `status` - status of the campaign, always `DRAFT` after creation * `version` - version identifiers for each email, ranging from `A` to `E` You can review the [full campaign payload here](campaigns.mdx#campaign-body-schema). Invalid request or malformed request syntax. Please review the [request body](#body). ```json { "code": "INPUT_DATA_VALIDATION_FAILURE", "message": "Input data validation failure", "details": "String" } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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](#endpoint) ```json { "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 ```json { "code": "VALIDATION_FAILURE", "message": "Validation failure", "details": "String" } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Unexpected error, please try again later. ```json { "code": "UNKNOWN", "message": "Unknown error during create campaign call", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | --- ## Edit campaign This action changes the status of a campaign to the `EDITED` status. It allows you to make changes to a campaign - modify its general settings, content of the emails, sending mailboxes, etc. Refer to other [/campaigns endpoints](campaigns.mdx) to edit the campaign. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/make_editable ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Edit a campaign ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/make_editable" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def makeCampaignEditable(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/make_editable" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.post(url, headers=headers) if response.status_code == 200: print("POST successful — campaign is now editable.") else: print("POST failed with status:", response.status_code) if __name__ == "__main__": makeCampaignEditable(123) # Example campaign ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; // Example campaign ID makeCampaignEditable(campaignId); } public static void makeCampaignEditable(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/make_editable"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST successful — campaign is now editable."); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function makeCampaignEditable(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/make_editable`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.post(url, null, { headers: headers }); if (response.status === 200) { console.log("POST successful — campaign is now editable."); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } makeCampaignEditable(123); // Example campaign ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $campaignId = '{campaign_id}'; try { $response = $client->post("campaigns/{$campaignId}/make_editable"); 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 The campaign status has been changed to `EDITED`. ``` Status: 200 Body: none ``` Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "title": "Bad Request", "status": 400, "detail": "Value of campaign_id is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Unexpected error, please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during make editable campaign call", "details": null } ``` --- ## Pause campaign This request will change the campaign status to `PAUSED` and will halt any further contact with prospects until the status is changed back to `RUNNING`. To resume a campaign, use the [/run endpoint](POST-run-campaign.mdx). ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/pause ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Pause a campaign ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/pause" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def pauseCampaign(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/pause" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.post(url, headers=headers) if response.status_code == 200: print("POST successful — campaign paused.") else: print("POST failed with status:", response.status_code) if __name__ == "__main__": pauseCampaign(123) # Example campaign ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; // Example campaign ID pauseCampaign(campaignId); } public static void pauseCampaign(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/pause"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST successful — campaign paused."); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function pauseCampaign(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/pause`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.post(url, null, { headers: headers }); if (response.status === 200) { console.log("POST successful — campaign paused."); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } pauseCampaign(123); // Example campaign ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $campaignId = '{campaign_id}'; try { $response = $client->post("campaigns/{$campaignId}/pause"); 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 The campaign has been paused. ``` Status: 200 Body: none ``` Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "title": "Bad Request", "status": 400, "detail": "Value of campaign_id is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Unexpected error, please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during pause campaign call", "details": null } ``` --- ## Run campaign This request starts a campaign and changes its status to `RUNNING`. Prospects enrolled in the campaign will be processed based on the campaign settings and delivery times. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/run ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Request title ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/run" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def runCampaign(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/run" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.post(url, headers=headers) if response.status_code == 200: print("POST successful — campaign is now running.") else: print("POST failed with status:", response.status_code) if __name__ == "__main__": runCampaign(123) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; runCampaign(campaignId); } public static void runCampaign(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/run"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST successful — campaign is now running."); } else { System.err.println("POST failed with status: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function runCampaign(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/run`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.post(url, null, { headers: headers }); if (response.status === 200) { console.log("POST successful — campaign is now running."); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } runCampaign(123); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $campaignId = '{campaign_id}'; try { $response = $client->post("campaigns/{$campaignId}/run"); 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 The campaign has been run. ``` Status: 200 Body: none ``` Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "title": "Bad Request", "status": 400, "detail": "Value of campaign_id is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 campaign uses features that are not covered by your current subscription. ```json { "code": "MISSING_FEATURES", "message": "Upgrade your plan", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | The requested campaign doesn't exist. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | The campaign configuration is invalid. This may be due to missing required fields or the use of features that are not currently supported by the API. Please review the [campaign configuration](campaigns.mdx) for more details. ```json { "code": "VALIDATION_FAILURE" | "API_UNSUPPORTED_CAMPAIGN_FEATURES", "message": "Validation failure" | "Campaign contains currently unsupported features", "details": "String" } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Unexpected error, please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during run campaign call", "details": null } ``` --- ## Stop campaign This request will change the campaign status to `STOPPED` and will halt any further contact with prospects until the status is changed back to `RUNNING`. To resume a campaign, use the [/run endpoint](POST-run-campaign.mdx). ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/stop ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Stop a campaign ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/stop" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def stopCampaign(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/stop" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.post(url, headers=headers) if response.status_code == 200: print("POST successful — campaign stopped.") else: print("POST failed with status:", response.status_code) if __name__ == "__main__": stopCampaign(123) # Example campaign ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int campaignId = 123; // Example campaign ID stopCampaign(campaignId); } public static void stopCampaign(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/stop"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.noBody()) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST successful — campaign stopped."); } else { System.err.println("POST failed with status: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function stopCampaign(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/stop`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.post(url, null, { headers: headers }); if (response.status === 200) { console.log("POST successful — campaign stopped."); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } stopCampaign(123); // Example campaign ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $campaignId = '{campaign_id}'; try { $response = $client->post("campaigns/{$campaignId}/stop"); 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 The campaign has been stopped. ``` Status: 200 Body: none ``` Invalid request or malformed syntax. Please review the [request](#endpoint) ```json { "title": "Bad Request", "status": 400, "detail": "Value of campaign_id is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | Unexpected error, please try again later. ```json { "type": "UNKNOWN", "message": "Unknown error during stop campaign call", "details": null } ``` --- ## Clear Bounce Shield threshold Clear the Bounce Shield Monitor bounce rate threshold from a campaign, effectively disabling the Bounce Shield Monitor. ## Request ### Endpoint ```text DELETE https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `campaign_id` | Yes | integer | Campaign ID | ### Request samples #### Clear the threshold ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def clear_bounce_shield_threshold(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.delete(url, headers=headers) if response.status_code == 204: print("Threshold cleared successfully.") else: print("DELETE failed with status:", response.status_code, response.text) if __name__ == "__main__": clear_bounce_shield_threshold(123) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { clearBounceShieldThreshold(123); } public static void clearBounceShieldThreshold(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/bounce_shield/threshold"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 204) { System.out.println("Threshold cleared successfully."); } else { System.err.println("DELETE request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function clearBounceShieldThreshold(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/bounce_shield/threshold`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.delete(url, { headers }); if (response.status === 204) { console.log("Threshold cleared successfully."); } else { console.error("DELETE failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } clearBounceShieldThreshold(123); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $campaignId = '{campaign_id}'; try { $response = $client->delete("campaigns/{$campaignId}/bounce_shield/threshold"); echo $response->getStatusCode(), "\n"; } catch (RequestException $e) { echo "Error: ", $e->getMessage(), "\n"; if ($e->hasResponse()) { echo $e->getResponse()->getBody(), "\n"; } } ``` ## Response ### Response examples The threshold has been cleared. ```text Status: 204 Body: none ``` An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 does not exist, was deleted, or belongs to another account. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An unknown error occurred while clearing the threshold. Please try again later. ```json { "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 | --- ## Get Bounce Shield threshold Retrieve the Bounce Shield Monitor bounce rate threshold configured for a campaign. The response returns an integer representing the percentage threshold, for example `10` for 10%, or `null` if no threshold is set for the campaign. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `campaign_id` | Yes | integer | Campaign ID | ### Request samples #### Retrieve the threshold ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def get_bounce_shield_threshold(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print(response.json()) else: print("GET failed with status:", response.status_code, response.text) if __name__ == "__main__": get_bounce_shield_threshold(123) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getBounceShieldThreshold(123); } public static void getBounceShieldThreshold(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/bounce_shield/threshold"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println(response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getBounceShieldThreshold(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/bounce_shield/threshold`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers }); if (response.status === 200) { console.log(response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getBounceShieldThreshold(123); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $campaignId = '{campaign_id}'; try { $response = $client->get("campaigns/{$campaignId}/bounce_shield/threshold"); 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 Request processed successfully. ```json { "bounce_rate_threshold": 12 } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `bounce_rate_threshold` | integer/null | Bounce rate threshold percentage configured for the campaign. Returns `null` when no threshold is set | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 does not exist, was deleted, or belongs to another account. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An unknown error occurred while retrieving the threshold. Please try again later. ```json { "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 | --- ## Set Bounce Shield threshold Set or replace the Bounce Shield Monitor bounce rate threshold for a campaign. Provide the threshold as an integer representing a percentage, for example `10` for 10%. Bounce Shield Monitor uses this value to decide when to automatically pause the campaign. ## Request ### Endpoint ```text PUT https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `campaign_id` | Yes | integer | Campaign ID | ### Body The request body is required. Send `bounce_rate_threshold` as an integer between `1` and `99`. ```json { "bounce_rate_threshold": 12 } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `bounce_rate_threshold` | integer | Yes | Bounce rate threshold percentage | ### Request samples #### Set the threshold ```bash curl --request PUT \ --url "https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "bounce_rate_threshold": 12 }' ``` ```python import requests def set_bounce_shield_threshold(campaign_id): url = f"https://api.woodpecker.co/rest/v2/campaigns/{campaign_id}/bounce_shield/threshold" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "bounce_rate_threshold": 12 } response = requests.put(url, headers=headers, json=payload) if response.status_code == 204: print("Threshold set successfully.") else: print("PUT failed with status:", response.status_code, response.text) if __name__ == "__main__": set_bounce_shield_threshold(123) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { setBounceShieldThreshold(123); } public static void setBounceShieldThreshold(int campaignId) { try { String url = "https://api.woodpecker.co/rest/v2/campaigns/" + campaignId + "/bounce_shield/threshold"; String jsonData = """ { "bounce_rate_threshold": 12 } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .PUT(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 204) { System.out.println("Threshold set successfully."); } else { System.err.println("PUT request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function setBounceShieldThreshold(campaignId) { const url = `https://api.woodpecker.co/rest/v2/campaigns/${campaignId}/bounce_shield/threshold`; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { bounce_rate_threshold: 12 }; try { const response = await axios.put(url, data, { headers }); if (response.status === 204) { console.log("Threshold set successfully."); } else { console.error("PUT failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } setBounceShieldThreshold(123); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $campaignId = '{campaign_id}'; try { $response = $client->put("campaigns/{$campaignId}/bounce_shield/threshold", [ 'json' => [ 'bounce_rate_threshold' => 12, ], ]); echo $response->getStatusCode(), "\n"; } catch (RequestException $e) { echo "Error: ", $e->getMessage(), "\n"; if ($e->hasResponse()) { echo $e->getResponse()->getBody(), "\n"; } } ``` ## Response ### Response examples The threshold has been set. ```text Status: 204 Body: none ``` Invalid request body or threshold value. ```json { "code": "INPUT_DATA_VALIDATION_FAILURE", "message": "Input data validation failure", "details": { "errors": [ { "field": "bounce_rate_threshold", "detail": "bounce_rate_threshold must be an integer between 1 and 99" } ] } } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | object | Additional information | |   └─ `errors` | array[object] | Validation errors | |   └─ `field` | string | Field that failed validation | |   └─ `detail` | string | Validation failure details | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 does not exist, was deleted, or belongs to another account. ```json { "code": "CAMPAIGN_NOT_EXIST", "message": "Campaign not found", "details": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `code` | string | Error code | | `message` | string | Error message | | `details` | string/null | Additional information | An unknown error occurred while setting the threshold. Please try again later. ```json { "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 | --- ## Bounce Shield Bounce Shield Monitor can automatically pause a campaign when the campaign bounce rate reaches the configured threshold. Use these endpoints to retrieve, set, or clear that threshold for a campaign. For product behavior and setup details, see the [Bounce Shield Monitor help article](https://woodpecker.co/help-center/en/articles/15228700-bounce-shield-monitor-in-woodpecker-campaigns). ## Available endpoints | Endpoint | Method and path | Use it to | |----------|-----------------|-----------| | [Get Bounce Shield threshold](GET-bounce-shield-threshold.mdx) | `GET /v2/campaigns/{campaign_id}/bounce_shield/threshold` | Retrieve the configured bounce rate threshold | | [Set Bounce Shield threshold](PUT-bounce-shield-threshold.mdx) | `PUT /v2/campaigns/{campaign_id}/bounce_shield/threshold` | Set or replace the campaign threshold | | [Clear Bounce Shield threshold](DELETE-bounce-shield-threshold.mdx) | `DELETE /v2/campaigns/{campaign_id}/bounce_shield/threshold` | Remove the campaign threshold | --- ## Campaigns The `v2/campaigns` API allows you to manage your **email and LinkedIn campaigns** in Woodpecker. You can: * create new email and LinkedIn campaigns; configure their settings, content, delivery times, follow-ups * fetch campaign statistics * change campaign statuses * edit existing campaigns - update content, adjust sending limits, modify campaign or step settings, and add new steps * delete campaigns or individual steps The current implementation focuses on linear campaigns with `EMAIL` and `LINKEDIN` steps. There are some features available in the Woodpecker app that are not supported by the API and will return a `409` error. Here is a list of such features:
Currently unsupported campaign features * Campaigns with an IF condition * Campaigns with a scheduled start * Campaigns with manual task steps * Snippet labels - campaigns that use [snippet labels](https://woodpecker.co/help-center/en/articles/5854053) in their content won't thrown an error but will return the original snippet value. Eg. \{\{SNIPPET_10\}\} instead of \{\{MY_CUSTOM_NAME\}\}
## Campaign body schema The campaign payload consists of several objects, each described in detail below. The included example shows a 3-step multichannel campaign as returned by a `GET` request — note that not all fields are required for `POST` or `PATCH` requests.
Example campaign payload ```json { "id": 12345679, "name": "Three step campaign with LinkedIn and email", "status": "RUNNING", "bounce_shield_autopaused_at": "2025-02-10T14:14:57+01:00", "email_account_ids": [123456, 123457, 123458], "settings": { "timezone": "Europe/Warsaw", "prospect_timezone": true, "daily_enroll": 30, "gdpr_unsubscribe": true, "list_unsubscribe": true, "open_disabled_list": ["google.com", "OTHER_PROVIDER"], "auto_pause_prospect_from_domain": true, "auto_pause_prospect_from_domain_statuses": ["REPLIED", "BOUNCED"], "catch_all_verification_mode": "MAXIMUM", "count_followup_delay_in_working_days": true }, "steps": { "id": "e688d52a-b867-4690-acf2-3809286915b1", "type": "START", "followup": { "id": "36c4fb2c-6f4f-45bf-aeae-4903501193hd", "type": "EMAIL", "delivery_time": { "MONDAY": [{ "from": "09:00", "to": "18:00" }], "TUESDAY": [{ "from": "09:00", "to": "18:00" }], "WEDNESDAY": [{ "from": "09:00", "to": "18:00" }], "THURSDAY": [{ "from": "09:00", "to": "18:00" }] }, "body": { "versions": [ { "id": "2af5c021295511bb54acb87b47c59c378a795f67f9a1b73dd34609c193e1664f", "version": "A", "subject": "Example subject line - version A", "message": "
Hi {{FIRST_NAME | \"there\"}},

This is an example cold email message. 

Best wishes, 
", "signature": "SENDER", "track_opens": true }, { "id": "c61b5583c7d19fd04d23b2181a17af640c4cf011490acd6f9e4537f62db0e7ba", "version": "B", "subject": "Example subject line - version B", "message": "
{{SPINTAX | \"Hi\" | \"Hello\" | \"Good morning\"}} {{FIRST_NAME}},

Yet another example of a cold email message. 

All the best, 
", "signature": "SENDER", "track_opens": false } ] }, "followup_after": { "range": "DAY", "value": 2 }, "followup": { "id": "a99e297f-8423-4600-8c24-5bc21b936302", "type": "LINKEDIN", "body": { "versions": [ { "id": "06cb0a70aba1dedae5a9eb949286fb2326f1e9105851a6be6f187e47c131c7be", "version": "A", "message": "" } ], "linkedin_account_id": 200002, "action_type": "CONNECTION_REQUEST" }, "followup_after": { "range": "DAY", "value": 4 }, "followup": { "id": "7371e283-6de9-440a-9eaf-273232b580f7", "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": [ { "id": "f88459226decae0e46c83d8c85b235f90b53097be8bdf7b4ecc7a6d7f4084324", "version": "A", "subject": null, "message": "
Email followup, sender's signature, no open tracking, same subject line
", "signature": "SENDER", "track_opens": false }, { "id": "740b243fe43ceca7f89f38d940b318e06e3acea404ce551191abe512bf4f4a50", "version": "B", "subject": "Subject linie 3B", "message": "
Email followup, no signature, no open tracking, different subject line
", "signature": "NO_SIGNATURE", "track_opens": false } ] }, "followup_after": { "range": "DAY", "value": 1 }, "followup": null } } } } } ```
### Campaign configuration object The root level of the campaign payload. It provides general information about the campaign and campaign-wide settings. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `id` | integer | - | Unique identifier of the campaign | | `name` | string | “My campaign #0” | Name of the campaign | | `status` | string | - | Current campaign status. Possible values: `RUNNING`, `DRAFT`, `STOPPED`, `PAUSED`, `EDITED`, `COMPLETED` | | `bounce_shield_autopaused_at` | string/null | null | Date and time when [Bounce Shield Monitor](https://woodpecker.co/help-center/en/articles/15228700-bounce-shield-monitor-in-woodpecker-campaigns) automatically paused the campaign after reaching the configured bounce rate threshold. Returned in ISO 8601 format. Returns `null` when the campaign has not been automatically paused | | `email_account_ids` | array[integer] | - | List of SMTP mailbox IDs used in this campaign. Use the `id` of a mailbox with `type: "SMTP"` returned by the [/mailboxes endpoint](/docs/mailboxes/mailboxes.md) | | `settings` | object | - | Campaign-level settings like timezone, sending limit, unsubscribe settings, etc | | └─`timezone` | string | - | 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 Africa/Abidjan Africa/Accra Africa/Addis_Ababa Africa/Asmara Africa/Bamako Africa/Bangui Africa/Banjul Africa/Bissau Africa/Blantyre Africa/Brazzaville Africa/Bujumbura Africa/Cairo Africa/Casablanca Africa/Conakry Africa/Dakar Africa/Dar_es_Salaam Africa/Djibouti Africa/El_Aaiun Africa/Freetown Africa/Gaborone Africa/Harare Africa/Johannesburg Africa/Kampala Africa/Khartoum Africa/Kigali Africa/Kinshasa Africa/Lagos Africa/Libreville Africa/Lome Africa/Luanda Africa/Lusaka Africa/Malabo Africa/Maputo Africa/Maseru Africa/Mbabane Africa/Mogadishu Africa/Monrovia Africa/Nairobi Africa/Ndjamena Africa/Niamey Africa/Nouakchott Africa/Ouagadougou Africa/Porto-Novo Africa/Sao_Tome Africa/Tripoli Africa/Tunis Africa/Windhoek America/Anchorage America/Anguilla America/Antigua America/Argentina/Buenos_Aires America/Aruba America/Asuncion America/Barbados America/Belize America/Bogota America/Buenos_Aires America/Caracas America/Cayenne America/Cayman America/Chicago America/Chihuahua America/Costa_Rica America/Denver America/Dominica America/Edmonton America/El_Salvador America/Godthab America/Grand_Turk America/Grenada America/Guadeloupe America/Guatemala America/Guayaquil America/Guyana America/Halifax America/Havana America/Indianapolis America/Jamaica America/La_Paz America/Lima America/Los_Angeles America/Managua America/Manaus America/Martinique America/Mazatlan America/Mexico_City America/Miquelon America/Monterrey America/Montevideo America/Montserrat America/Nassau America/New_York America/Panama America/Paramaribo America/Phoenix America/Port-au-Prince America/Port_of_Spain America/Puerto_Rico America/Regina America/Rio_Branco America/Santiago America/Santo_Domingo America/Sao_Paulo America/St_Johns America/St_Kitts America/St_Lucia America/St_Thomas America/St_Vincent America/Tegucigalpa America/Tijuana America/Toronto America/Tortola America/Vancouver America/Whitehorse America/Winnipeg Arctic/Longyearbyen Asia/Aden Asia/Almaty Asia/Amman Asia/Ashgabat Asia/Baghdad Asia/Bahrain Asia/Baku Asia/Bangkok Asia/Beirut Asia/Bishkek Asia/Brunei Asia/Calcutta Asia/Chongqing Asia/Colombo Asia/Damascus Asia/Dhaka Asia/Dubai Asia/Dushanbe Asia/Gaza Asia/Hong_Kong Asia/Irkutsk Asia/Istanbul Asia/Jakarta Asia/Jerusalem Asia/Kamchatka Asia/Karachi Asia/Kathmandu Asia/Kolkata Asia/Krasnoyarsk Asia/Kuala_Lumpur Asia/Kuwait Asia/Macau Asia/Magadan Asia/Manila Asia/Muscat Asia/Nicosia Asia/Novosibirsk Asia/Omsk Asia/Phnom_Penh Asia/Pyongyang Asia/Qatar Asia/Rangoon Asia/Riyadh Asia/Seoul Asia/Shanghai Asia/Singapore Asia/Taipei Asia/Tashkent Asia/Tbilisi Asia/Tehran Asia/Thimphu Asia/Tokyo Asia/Ulaanbaatar Asia/Ulan_Bator Asia/Urumqi Asia/Vientiane Asia/Vladivostok Asia/Yakutsk Asia/Yerevan Atlantic/Azores Atlantic/Bermuda Atlantic/Cape_Verde Atlantic/Faroe Atlantic/Reykjavik Atlantic/South_Georgia Atlantic/St_Helena Atlantic/Stanley Australia/Adelaide Australia/Brisbane Australia/Canberra Australia/Darwin Australia/Hobart Australia/Melbourne Australia/Perth Australia/Sydney Canada/Atlantic Canada/Eastern Canada/Mountain Canada/Newfoundland Canada/Saskatchewan Europe/Amsterdam Europe/Athens Europe/Belgrade Europe/Berlin Europe/Bratislava Europe/Brussels Europe/Bucharest Europe/Budapest Europe/Chisinau Europe/Copenhagen Europe/Dublin Europe/Gibraltar Europe/Guernsey Europe/Helsinki Europe/Isle_of_Man Europe/Istanbul Europe/Jersey Europe/Kiev Europe/Lisbon Europe/Ljubljana Europe/London Europe/Luxembourg Europe/Madrid Europe/Malta Europe/Minsk Europe/Monaco Europe/Moscow Europe/Oslo Europe/Paris Europe/Podgorica Europe/Prague Europe/Riga Europe/Rome Europe/San_Marino Europe/Sarajevo Europe/Skopje Europe/Sofia Europe/Stockholm Europe/Tallinn Europe/Vaduz Europe/Vatican Europe/Vienna Europe/Vilnius Europe/Volgograd Europe/Warsaw Europe/Zagreb Europe/Zurich Hongkong Indian/Antananarivo Indian/Chagos Indian/Christmas Indian/Cocos Indian/Comoro Indian/Kerguelen Indian/Mahe Indian/Maldives Indian/Mauritius Indian/Mayotte Indian/Reunion Pacific/Auckland Pacific/Efate Pacific/Fakaofo Pacific/Fiji Pacific/Funafuti Pacific/Guadalcanal Pacific/Guam Pacific/Honolulu Pacific/Kiritimati Pacific/Midway Pacific/Nauru Pacific/Niue Pacific/Norfolk Pacific/Noumea Pacific/Palau Pacific/Pitcairn Pacific/Pohnpei Pacific/Port_Moresby Pacific/Rarotonga Pacific/Saipan Pacific/Tahiti Pacific/Tarawa Pacific/Tongatapu Pacific/Wallis Singapore US/Alaska US/Arizona US/Central US/East-Indiana US/Eastern US/Hawaii US/Mountain US/Samoa UTC
| | └─`prospect_timezone` | boolean | false | Whether to adjust sending times to prospect's timezone instead of the campaign `timezone`. Applies to `EMAIL` steps | | └─`daily_enroll` | integer | - | Maximum number of prospects that can be contacted in the opening step of the campaign per day. This limit is applied per mailbox or LinkedIn account. The default maximum value is 500 | | └─`gdpr_unsubscribe` | boolean | false | Whether the unsubscribe link should provide prospects with an option for [GDPR-compliant data removal](https://woodpecker.co/help-center/en/articles/5258897). This option will work only if the \{\{UNSUBSCRIBE\}\} snippet is included in your email or account signature | | └─`list_unsubscribe` | boolean | false | Whether to include [List-Unsubscribe header](https://woodpecker.co/help-center/en/articles/5258897). This option will work only if the \{\{UNSUBSCRIBE\}\} snippet is included in your email or account signature | | └─`open_disabled_list` | array[string] | [] | 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 | false | Legacy flag kept for older campaigns. For new API integrations, use `auto_pause_prospect_from_domain_statuses` | | └─`auto_pause_prospect_from_domain_statuses` | array[string] | [] | Prospect statuses that trigger same-domain auto-pause. Allowed values: `REPLIED`, `BOUNCED`. When enabled, if one prospect replies or bounces, Woodpecker will pause other prospects from that domain in a given campaign. Common free domains such as `gmail.com` and `outlook.com` are excluded. When present, this field takes precedence over `auto_pause_prospect_from_domain` | | └─`catch_all_verification_mode` | string | `BALANCED` | [Catch-all email verification mode](https://woodpecker.co/help-center/en/articles/10233496) - how to approach contacting prospects using catch-all emails. `NONE` - contact all catch-all emails, including undeliverable `BALANCED` - contact deliverable and risky catch-all emails `MAXIMUM` - contact only deliverable catch-all emails `ONLY_VERIFY` - do not contact catch-all emails | | └─`count_followup_delay_in_working_days` | boolean | false | Whether follow-up delays count only working days. When `true`, Saturdays and Sundays are skipped while the original sending time is preserved. When `false`, weekends count toward the delay. [Learn more](https://woodpecker.co/help-center/en/articles/5258787#h_23ad4abb90) | | `steps` | object | - | Campaign steps, including all LinkedIn actions or emails and their delivery times, content, etc | ### Step objects Steps define the structure of a campaign and the actions for each prospect. Every campaign must begin with a `START` step, followed by 1 to 16 `EMAIL` or `LINKEDIN` 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. The `START` step must always be the first (root) step of the campaign and cannot occur elsewhere. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `id` | string | - | Unique identifier of the step (UUID) | | `type` | string | - | Use `START` to indicate a start step | | `followup` | object | - | The next step in the sequence. For a `START` step, this field is required and must point to the first `EMAIL` or `LINKEDIN` step sent to prospects | This type defines an email step of a campaign, including its content, versions, delivery times, and follow-ups. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `id` | string | - | Unique identifier of the step (UUID) | | `type` | string | - | Use `EMAIL` to indicate it is an email step | | `delivery_time` | object | - | Time intervals during which emails can be sent. Described in more detail [below](#delivery-time-object) | | `body` | object | - | Email content configuration including A/B test versions. Described in more detail [below](#body-object) | | `followup_after` | object | 1 DAY | Object that specifies the time delay before processing a prospect in the next step; if `delivery_time` allows it. If not provided, a default delay of `1 DAY` will be applied | | └─`range` | string | DAY | Time unit: `DAY`, `HOUR`, `MINUTE` | | └─`value` | integer | 1 | Value of the time unit (range: 1 - 9999) | | `followup` | object/null | null (meaning no followup) | Next step in the sequence. Should consist of an `EMAIL` or `LINKEDIN` step object. Null indicates end of sequence |
Example step object `Path: steps.followup` ```json { "id": "169486a5-e375-48cd-81a1-01a7f2a1895f", "type": "EMAIL", "delivery_time": "...", // See delivery_time schema "body": "...", // See body schema "followup_after": { "range": "DAY", "value": 1 }, "followup": "..." // the next step of the campaign } ```
#### 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](#campaign-configuration-object). Each step must define at least one delivery interval. You can assign up to three intervals per day, but they must not overlap. If a day is omitted from the object, no emails will be sent on that day. To specify a whole day interval, you can use either `"from": "00:00", "to": "00:00"` or `"from": "00:00", "to": "24:00"`. The first format is set as the default. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `MONDAY`...`SUNDAY` | array[object] | - | Array of time windows for each day. Maximum 3 windows per day. The valid keys are the days of the week: `MONDAY`, `TUESDAY`, `WEDNESDAY`, `THURSDAY`, `FRIDAY`, `SATURDAY`, `SUNDAY` | | └─`[].from` | string | - | Start time in "HH:mm" format (24-hour) | | └─`[].to` | string | - | End time in "HH:mm" format (24-hour) |
Example delivery_time object `Path: steps.followup.delivery_time` ```json { "WEDNESDAY": [ { "from": "09:00", "to": "17:00" } ], "THURSDAY": [ { "from": "09:00", "to": "11:00" }, { "from": "14:00", "to": "16:00" } ] } ```
### Body object The `body` and `versions` objects define the email content, A/B 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 | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of email version objects and their definitions. At least one version is required | | └─`[].id` | string | - | Unique identifier of the email version | | └─`[].version` | string | A | Version Identifier. The default version is `A`. Available versions are `A` through `E`. When creating a campaign, the versions are determined by their order in the array, not by explicit declaration | | └─`[].subject` | string/null | - | Email subject line. Required for the first `EMAIL` step. If multiple versions exist, all must include a subject. For later `EMAIL` steps, a null subject sends the message as a follow-up in the same thread. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | └─`[].message` | string | - | Email body content in HTML format. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884). To track individual link clicks ([not recommended](https://woodpecker.co/help-center/en/articles/5267688)), enclose the href attribute value in a \{\{CLICK\}\} snippet. Example: `click here` | | └─`[].signature` | string | NO_SIGNATURE | Whether to use the sender's email account signature. The available options are: `SENDER` or `NO_SIGNATURE` | | └─`[].track_opens` | boolean | false | Whether to track email opens for this email version |
Example body object `Path: steps.followup.body` ```json { "versions": [ { "id": "06cb0a70aba1dedae5a9eb949286fb2326f1e9105851a6be6f187e47c131c7be", "version": "A", "subject": null, "message": "
First followup, version A, sender's signature, no open tracking, same subject line
", "signature": "SENDER", "track_opens": false }, { "id": "4d7d78eebd5abe71eee873b5da40d525e16f0f3611a8a543cae274a5a772a54f", "version": "B", "subject": null, "message": "
First followup, version B, no signature, no open tracking, same subject line
", "signature": "NO_SIGNATURE", "track_opens": false }, { "id": "4ac2bec5a9082b090aad90eeaa230fe08e7a5ab3a6b968d9328ec437316c9037", "version": "C", "subject": "A new email subject", "message": "
First followup, version C, sender's signature, open tracking, different subject line - sent in the same thread
", "signature": "SENDER", "track_opens": true } ] } ```
This type defines a linkedin step of a campaign, including its action type, content, versions, and follow-ups. You can define the action to perform within the `body`. Available actions: `VISIT_PROFILE`, `CONNECTION_REQUEST`, `DIRECT_MESSAGE`, `INMAIL_MESSAGE` | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `id` | string | - | Unique identifier of the step (UUID) | | `type` | string | - | Use `LINKEDIN` to indicate it is a linkedin step | | `body` | object | - | LinkedIn acton configuration. Described in more detail [below](#body-object-1) | | `followup_after` | object | 1 DAY | Object that specifies the time delay before processing a prospect in the next step. If not provided, a default delay of `1 DAY` will be applied | | └─`range` | string | DAY | Time unit: `DAY`, `HOUR`, `MINUTE` | | └─`value` | integer | 1 | Value of the time unit (range: 1 - 9999) | | `followup` | object/null | null (meaning no followup) | Next step in the sequence. Should consist of an `EMAIL` or `LINKEDIN` step object. Null indicates end of sequence |
Example step object `Path: steps.followup` ```json { "id": "169486a5-e375-48cd-81a1-01a7f2a1895f", "type": "LINKEDIN", "body": "...", // See body schema "followup_after": { "range": "DAY", "value": 1 }, "followup": "..." // the next step of the campaign } ```
### Body object The `body` and `versions` define the LinkedIn action type and its content. All action types share the same data structure but differ in their use of `version`, `subject`, and `message` fields. Supported actions are: `VISIT_PROFILE`, `CONNECTION_REQUEST`, `DIRECT_MESSAGE`, and `INMAIL_MESSAGE`. | Field | Type | Default | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of LinkedIn action version objects. Only one boilerplate version will be returned for `VISIT_PROFILE` | | └─`[].id` | string | - | Unique identifier of the LinkedIn action version | | └─`[].version` | string | A | Version Identifier. For `VISIT_PROFILE` value will always be `A` | | └─`[].message` | string | "" | For `VISIT_PROFILE` value will always be an empty string | | `body.linkedin_account_id` | integer | null | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Action type that will be performed in LinkedIn: `VISIT_PROFILE` |
Example body object `Path: steps.followup.body` ```json { "versions": [ { "id": "0989bc853013413bc1f46fb0176915dd8536d33d19df583832e82bc3f6207c57", "version": "A", "message": "" } ], "linkedin_account_id": 200002, "action_type": "VISIT_PROFILE" } ```
| Field | Type | Default | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of LinkedIn action version objects. At least one version is required | | └─`[].id` | string | - | Unique identifier of the LinkedIn action version | | └─`[].version` | string | A | Version Identifier. The default version is `A`. Available versions are `A` through `E`. When creating a campaign, the versions are determined by their order in the array, not by explicit declaration | | └─`[].message` | string | "" | Connection request message content. An empty string (`""`) sends a connection request without a message; otherwise, the provided note will be included with the request. Character limits: `CLASSIC` accounts - 200 characters, `PREMIUM`, `RECRUITER_LITE`, `SALES_NAVIGATOR` - 300 characters. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `body.linkedin_account_id` | integer | null | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Action type that will be performed in LinkedIn: `CONNECTION_REQUEST` |
Example body object `Path: steps.followup.body` ```json { "versions": [ { "id": "0303e56ea29da51e268b1af71743ec8781e7c045daebeec8f1b7c10413594d0e", "version": "A", "message": "" }, { "id": "b898c9849995032a4626b4c84433b3aa5a7b4c80fe766d1d172cf1b5b3236641", "version": "B", "message": "Hi {{FIRST_NAME}}, I though we could connect" } ], "linkedin_account_id": 200002, "action_type": "CONNECTION_REQUEST" } ```
| Field | Type | Default | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of LinkedIn action version objects. At least one version is required | | └─`[].id` | string | - | Unique identifier of the LinkedIn action version | | └─`[].version` | string | A | Version Identifier. The default version is `A`. Available versions are `A` through `E`. When creating a campaign, the versions are determined by their order in the array, not by explicit declaration | | └─`[].message` | string | - | Direct message content. Unlike other actions, message content is required. Character limit: 6000 characters. Supports snippets like \{\{FIRST_NAME\}\}, [snippet fallbacks](https://woodpecker.co/help-center/en/articles/6636519) and [spintax](https://woodpecker.co/help-center/en/articles/9973884) | | `body.linkedin_account_id` | integer | null | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Action type that will be performed in LinkedIn: `DIRECT_MESSAGE` |
Example body object `Path: steps.followup.body` ```json { "versions": [ { "id": "0303e56ea29da51e268b1af71743ec8781e7c045daebeec8f1b7c10413594d0e", "version": "A", "message": "Hi {{FIRST_NAME}} [...]" }, { "id": "b898c9849995032a4626b4c84433b3aa5a7b4c80fe766d1d172cf1b5b3236641", "version": "B", "message": "Hello {{SPINTAX | \"there\" | \"again\"}} [...]" } ], "linkedin_account_id": 200002, "action_type": "DIRECT_MESSAGE" } ```
| Field | Type | Default | Description | |-------|------|:---------:|-------------| | `body.versions` | array[object] | - | Array of LinkedIn action version objects. At least one version is required | | └─`[].id` | string | - | Unique identifier of the LinkedIn action version | | └─`[].version` | string | A | Version Identifier. The default version is `A`. Available versions are `A` through `E`. When creating a campaign, the versions are determined by their order in the array, not by explicit declaration | | └─`[].subject` | string/null | null | InMail subject. If present, it cannot contain snippets and must be at most 200 characters | | └─`[].message` | string | - | InMail message content. Message content is required. Character limit: 1900 characters | | `body.linkedin_account_id` | integer | null | Unique ID of a LinkedIn account in Woodpecker that will perform the action. Use [/linkedin_accounts endpoint](/docs/linkedin/get-linkedin-accounts.mdx) to review it | | `body.action_type` | string | - | Action type that will be performed in LinkedIn: `INMAIL_MESSAGE` |
Example body object `Path: steps.followup.body` ```json { "versions": [ { "id": "0303e56ea29da51e268b1af71743ec8781e7c045daebeec8f1b7c10413594d0e", "version": "A", "subject": "Quick question about your outbound workflow", "message": "Hi {{FIRST_NAME}}, I noticed your team is growing fast and thought Woodpecker could help streamline outbound follow-ups." } ], "linkedin_account_id": 200002, "action_type": "INMAIL_MESSAGE" } ```
--- ## Get inbox messages Retrieve a paginated list of email messages available in the Woodpecker inbox. You can use the available [parameters](#parameters) to narrow down the results by mailbox, campaign, prospect status, or interest level. In standard Woodpecker accounts and agency HQs, the `/inbox` endpoint lets you access emails from mailboxes connected by the owner of the API key. In agency client accounts, it provides access to all connected mailboxes. If you'd like to retrieve responses for a specific prospect, use the [v2/prospects endpoint](/docs/prospects/GET-prospect-responses.mdx) ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/inbox/messages ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters You can use the query parameters below to filter inbox messages. Each filter is optional - if you omit a parameter, that filter will not be applied. If no parameters are provided, the request will return unfiltered inbox messages based on the default pagination settings. | Parameter | Required | Type | Description | | ---------- | -------- | ---- | -------------------------------------------------- | | `prospect_status` | No | string | Filters inbox messages by the prospect's current status. Available values: `RESPONDED`, `AUTOREPLIED`, `BOUNCED`, `BLACKLISTED`, `OPT_OUT`. Mutually exclusive with `prospect_interest_level` and `out_of_campaign` | | `prospect_interest_level` | No | string | Filters inbox messages by the prospect's interest level. Available values: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED`, `NOT_MARKED`. Mutually exclusive with `prospect_status` and `out_of_campaign` | | `mailbox_ids` | No | string (comma-separated integers) | Retrieve only responses fetched from the specified mailbox IMAP accounts. Use [/mailboxes endpoint](/docs/mailboxes/mailboxes.md) to fetch mailbox details | | `campaign_ids` | No | string (comma-separated integers) | Retrieve only responses associated with given campaigns. Use [/campaign endpoints](/docs/campaigns/campaigns.mdx) to fetch campaign details | | `search_phrases` | No | array[string] | Filters messages using up to 20 search phrases. Phrases must be at least 3 characters long. Phrases can be provided as a comma-separated list `search_phrases=one,two,three` with a combined total length of 100 characters, or as repeated parameters `search_phrases=one&search_phrases=two`, where each phrase can be up to 100 characters long| | `read` | No | boolean | Whether to list messages that are marked as read or unread. If not specified, both will be included. | | `out_of_campaign` | No | string | Filters inbox messages that are not associated with any prospects. Accepted value: `YES`. Mutually exclusive with `prospect_status` and `prospect_interest_level` | | `per_page` | No | integer | Number of records per page. Default: 10, maximum: 50 | | `next_page_cursor` | No | string | Cursor used to retrieve a specific page of results. Use `null` to start from the first page. Only values returned in the `next_page_cursor` field of a previous response are accepted. Mutually exclusive with `previous_page_cursor`, but both can be `null`. See the [pagination section](#pagination) for more details | | `previous_page_cursor` | No | string | Cursor used to retrieve the previous page of results. Use `null` to start from the first page. Mutually exclusive with `next_page_cursor`, but both can be `null`. Only values returned in the `previous_page_cursor` field of a previous response are accepted. | {/* \*Note: The `prospect_status`, `prospect_interest_level` and `out_of_campaign` parameters are mutually exclusive - you can use only one of them in a request. Providing more than one of them will result in an error. If neither parameter is provided, these filters will not be applied. */} ### Request samples #### Retrieve 10 latest responses marked as INTERESTED ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/inbox/messages?per_page=5&prospect_interest_level=INTERESTED" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_interested_messages(): url = "https://api.woodpecker.co/rest/v2/inbox/messages?per_page=5&prospect_interest_level=INTERESTED" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_interested_messages() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/inbox/messages?per_page=5&prospect_interest_level=INTERESTED"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new Exception("GET request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function getInterestedMessages() { const url = 'https://api.woodpecker.co/rest/v2/inbox/messages?per_page=5&prospect_interest_level=INTERESTED'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getInterestedMessages(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('inbox/messages', [ 'query' => [ 'per_page' => 5, 'prospect_interest_level' => 'INTERESTED', ], ]); 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 A list of responses retrieved from the Woodpecker inbox. The responses are sorted by received date in descending order (newest first) ```json { "content": [ { "id": 123456, "prospect_id": 987654, "read": true, "bounced": false, "global_prospect_status": "ACTIVE", "stamp": "2026-03-05T17:57:00Z", "subject": "Re: Subject line of the response", "body": { "html": "
This is a reply in HTML format.
" }, "campaigns": [ { "id": 1763200, "name": "Associated campaign name", "status": "RUNNING", "prospect": { "campaign_prospect_id": 654321, "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "status": "PAUSED", "resume_followup_after": "2026-03-12T00:00:00Z", "status_reasons": [ { "reason": "AUTOREPLIED" } ], "substatus": { "created_at": "2026-01-15T14:00:00.000Z", "id": 10123, "name": "Q1 Contact" } } } ], "campaign": { "id": 1763200, "name": "Associated campaign name" }, "from_name": "John Smith", "from_email": "john@prospect.com", "recipient": "receiving@email.com", "mailbox_id": 112233, "cc": ["steve@prospect.com", "jimothy@prospect.com"], "not_found": false } ], "pagination": { "previous_page_cursor": null, "next_page_cursor": "Y3VyaW91cywgYXJlbid0IHlvdT8=" } } ``` ### Body schema | Field | Type | Description | |--------------------|---------------------|------------------------------------------------------------| | `content` | array[object] | Array of email messages | | └─`[].id` | integer | Unique ID of the prospect's response | | └─`[].prospect_id` | integer/null | Unique ID of the prospect associated with the response | | └─`[].global_prospect_status` | string/null | Prospect's **global** status: `ACTIVE`, `BOUNCED`, `RESPONDED`, `BLACKLIST`, `INVALID`, `OPT_OUT`. Available if `prospect_id` is not null | | └─`[].read` | boolean | Whether the message was marked as read | | └─`[].bounced` | boolean | Whehter the message was bounced | | └─`[].stamp` | string | ISO 8601 timestamp of the message in UTC | | └─`[].subject` | string | Subject line of the response | | └─`[].body.html` | string | Body of the response email in HTML format | | └─`[].campaigns` | array[object] | List of campaigns associated with the response. One response can be assigned to multiple campaigns | |     └─`[].id` | integer | ID of the campaign associated with a response | |     └─`[].name` | string | Name of the campaign | |     └─`[].status` | string | Current status of the campaign. Available values: `RUNNING`, `DRAFT`, `STOPPED`, `PAUSED`, `EDITED`, `COMPLETED`, `DELETED` | |     └─`[].prospect` | object | Campaign-level prospect information related to this message | |         └─`campaign_prospect_id` | integer | ID of the campaign prospect. Different than global `prospect_id` | |         └─`interest_level` | object | Interest level details for given prospect in associated campaign | |             └─`level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED`, `NOT_MARKED` | |             └─`ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |         └─`status` | string | Prospect's **campaign** status: `ACTIVE`, `BOUNCED`, `RESPONDED`, `BLACKLIST`, `OPT_OUT`, `PAUSED`, `INVALID`, `NON_RESPONSIVE`. `REMOVED` | |         └─`resume_followup_after` | string | The earliest date and time prospects can be contacted, for example to followup after an autoresponder. ISO 8601 timestamp | |         └─`status_reasons` | array[object] | List of reasons clarifying the prospect's status | |             └─`reason` | string | Values can contain: `SECONDARY_REPLY`, `OTHER_CAMPAIGN`, `AUTOREPLIED`, `MANUAL`, `SNIPPET_ISSUE`, `CATCH_ALL_DELIVERABLE`, `CATCH_ALL_RISKY`, `CATCH_ALL_UNDELIVERABLE`, `ESP_MATCHING` | |         └─`substatus` | object/null | Details about a substatus assigned to a prospect in a campaign. Read more here | |             └─`id` | integer | ID of the substatus | |             └─`name` | string | Name of the assigned substatus | |             └─`created_at` | string | ISO 8601 timestamp of when the substatus was first created | | └─`[].campaign` | object | Object containing a campaign related to the response. Using `campaigns` instead is recommended. | |     └─`id` | integer/null | ID of the campaign associated to a response | |     └─`name` | string/null | Name of the campaign | | └─`[].from_name` | string | Name of the responder | | └─`[].from_email` | string | Email address of the responder | | └─`[].recipient` | string | Email address of the recipient (typically, the IMAP of the mailbox that sent the email) | | └─`[].mailbox_id` | integer | Id of the IMAP mailbox that received that message | | └─`[].cc` | array[string] | List of email addresses that were added to the CC of the received email | | └─`[].not_found` | boolean | Indicator whether the message content was unavailable to display. If true, try again later | | `pagination` | object | Object containing [pagination information](#pagination) | | └─`next_page_cursor` | string | Pagination cursor. `null` means no more results (last page); non-null value indicates the cursor for the next page | | └─`previous_page_cursor` | string | Pagination cursor. `null` means first page; non-null value indicates the cursor for previous page | Invalid request parameters or malformed request syntax. Example messages are provided below; other variations are also possible ```json { "title": "Bad Request", "status": 400, "detail": "Query param 'per_page' value '0' must be greater or equal to 1" | "Query param {param} unsupported value {value}" | "string", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ### Pagination This endpoint uses cursor-based pagination. Results are returned in pages, sorted by received date in descending order (newest first), and each response includes a `next_page_cursor` field: * To start from the first page, omit the `next_page_cursor` parameter or set it to null. * If `next_page_cursor` are `null`, there are no more pages available, * If `next_page_cursor` or `previous_page_cursor` contain a value, you can use it in the next request to retrieve the next or previous page of results, **Request first page:** ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/inbox/messages?prospect_interest_level=INTERESTED" \ --header "x-api-key: {YOUR_API_KEY}" ``` Example response: ```json { "content": [...], "pagination": { "next_page_cursor": "T2ggSGkgTWFyayE=", "previous_page_cursor": null } } ``` **Request the next page:** ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/inbox/messages?next_page_cursor=T2ggSGkgTWFyayE=&prospect_interest_level=INTERESTED" \ --header "x-api-key: {YOUR_API_KEY}" ``` Example response: ```json { "content": [...], "pagination": { "next_page_cursor": "TSXQncyBhIHRyYXAh", "previous_page_cursor": "RG9uJ3QgdGVsbCBhbnlvbmUh" } } ``` **Request the previous page:** ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/inbox/messages?next_page_cursor=RG9uJ3QgdGVsbCBhbnlvbmUh&prospect_interest_level=INTERESTED" \ --header "x-api-key: {YOUR_API_KEY}" ``` --- ## Inbox The Inbox API allows you to retrieve and reply to messages received in your Woodpecker inbox. You can: - **List inbox messages** using the [v2/inbox/messages](/docs/inbox/get-inbox-messages) endpoint to browse or filter replies from prospects across all campaigns, - **Send a reply** to a specific message using the [v2/inbox/messages/\{id\}/reply](/docs/inbox/post-reply-message.mdx) endpoint - when you want to respond directly to a prospect’s email, via one of your connected SMTP mailboxes, - If you need to fetch all replies related to a specific prospect, use the dedicated [v2/prospects/\{pid\}/responses](/docs/prospects/GET-prospect-responses.mdx) endpoint. --- ## Reply to a message This endpoint allows you to reply to a message retrieved from the Woodpecker inbox. You can define the reply content, subject, recipients, and whether the original inbox message should be quoted below your reply. By default, the reply will be sent from the same mailbox that received the original message, but you can specify a different connected SMTP mailbox. Woodpecker automatically includes standard email reply threading headers, so the reply is sent in the same conversation thread. Set `quote_original_message` to `true` to include the original message as a quotation in the sent email. This helps the recipient see their previous reply directly in the conversation. :::tip Responding to a message automatically marks the received response as 'read' in the inbox ::: ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/inbox/messages/{id}/reply ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` ### Parameters | Parameter | Required | Type | Description | | ---------- | :-------- | ---- | ------------------------------------------------------- | | `id` | Yes | integer | Path parameter - ID of the prospect's response that will be replied to | For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "mailbox_id": 123456, "to": "recipient@gmail.com", "cc": "cc1@gmail.com,cc2@gmail.com", "bcc": "bcc1@gmail.com,bcc2@gmail.com", "subject": "Re: Example subject", "body": { "html": "

Example response here

" }, "quote_original_message": true } ``` #### Body schema | Field | Required | Type | Description | |------------|--------|-----|------------| | `mailbox_id` | No | integer | Defaults to the mailbox that received the original message. When specified, identifies the SMTP ID of a mailbox used to send the reply. Use [/mailboxes endpoint](/docs/mailboxes/mailboxes.md) to fetch mailbox details | | `to` | No | string | By default, the reply will be sent to the original author of the message. If provided, this field specifies the recipient's email addresses | | `cc` | No | string | Comma-separated list of email addresses to include in CC | | `bcc` | No | string | Comma-separated list of email addresses to include in BCC. If not provided, the BCC addresses configured in the mailbox settings (if any) will be used | | `subject` | No | string | Subject of the reply message. If not provided, the subject of the original message will be used, prefixed with `Re:` if not already present | | `body` | Yes | object | Object containing the message content | | └─ `html` | Yes | string | Content of the response email in HTML format | | `quote_original_message` | No | boolean | When `true`, includes the original inbox message as a quotation in the sent email. Default: `false` | ### Request samples #### Reply to a message ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/inbox/messages/{id}/reply" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "body": { "html": "

Example response here

" }, "quote_original_message": true }' ``` ```Python import requests def reply_to_message(): url = "https://api.woodpecker.co/rest/v2/inbox/messages/{id}/reply" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "body": { "html": "

Example response here

" }, "quote_original_message": True } try: response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST response:", response.json()) else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") except Exception as e: print("Error:", e) if __name__ == "__main__": reply_to_message() ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; import java.net.http.HttpRequest.BodyPublishers; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/inbox/messages/{id}/reply"; String json = """ { "body": { "html": "

Example response here

" }, "quote_original_message": true } """; 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(BodyPublishers.ofString(json)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { throw new Exception("POST request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function replyToMessage() { const url = 'https://api.woodpecker.co/rest/v2/inbox/messages/{id}/reply'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { body: { html: '

Example response here

' }, quote_original_message: true }; try { const response = await axios.post(url, data, { headers }); if (response.status === 200) { console.log('POST response:', response.data); } else { throw new Error(`POST request failed: ${response.status}, ${response.data}`); } } catch (error) { console.error('Error:', error.response ? error.response.status : error.message); } } replyToMessage(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $messageId = '{id}'; try { $response = $client->post("inbox/messages/{$messageId}/reply", [ 'json' => [ 'body' => [ 'html' => '

Example response here

', ], 'quote_original_message' => true, ], ]); 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 Message passed to SMTP for delivery ``` Status: 200 Body: none ``` Invalid request or malformed request syntax. Example messages are provided below; other variations are also possible ```json { "title": "Bad Request", "status": 400, "detail": "Invalid request body" | "Bcc emails if provided must contain comma-separated valid email addresses" | "Recipient email if provided must be valid email address", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 mailbox does not exist, or the request URL is incorrect. ```json { "title": "Not Found", "status": 404, "detail": "Mailbox not found" | "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Lead Finder Lead Finder is an API workflow for finding potential clients and turning them into richer prospect records. It has two entry points: search for new leads first, or enrich prospects that already exist in your Woodpecker database. Use **lead search and enrichment** when you want to build a new audience: for example, founders in a selected industry, managers in a specific country, or personas matching another set of Lead Finder criteria. These leads are not coming from the main Woodpecker prospects tab yet. You discover them through Lead Finder search, then queue selected lead records for enrichment. Use **prospect enrichment** when the prospects are already in your Woodpecker prospect database and can be found through the [Prospects](/docs/prospects/prospects.mdx) endpoints. In this flow, you submit existing prospects by email, with any helpful context such as name, LinkedIn URL, company name, or company website. The enrichment job works on matching existing prospect records; it is not a discovery search. ## How the flow works Lead Finder enrichment is asynchronous. Queue endpoints return a batch `uuid` and a queue status, not the finished enrichment result. Store that `uuid`, then poll the relevant list or get endpoint until the records finish processing. :::info Credit usage Some Lead Finder requests use account credits. Searching for leads and viewing returned lead data costs 1 credit per lead. For enrichment requests, credits are applied only when enrichment finds data: 1.5 credits for enriching a lead and finding an email, or 1.5 credits for successfully enriching an existing prospect. GET endpoints used to read criteria or check enrichment status do not use credits. ::: For new leads, the usual flow is: 1. get the available search criteria 2. fetch allowed values for controlled criteria 3. search for matching leads 4. queue selected leads for enrichment 5. check the enrichment batch until it is processed and available in your campaigns or the prospect database For existing prospects, start from the prospects already stored in Woodpecker, queue them for enrichment, then check the returned batch or status records until processing finishes. ## Next steps Read [Lead search and enrichment](lead-search-and-enrichment/overview.md) if you want to discover and enrich new people. Read [Prospect enrichment](prospect-enrichment/overview.md) if you want to enrich records already present in your Woodpecker prospect database. --- ## Get lead enrichment Retrieve one lead enrichment batch by its `uuid`. Use this endpoint after [queueing lead enrichments](queue-lead-enrichments.mdx) when you already know the batch ID and want the most direct status check. If you need to browse recent batches first, use [list lead enrichments](list-lead-enrichments.mdx). ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments/{uuid} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `uuid` | Yes | string | Enrichment batch UUID returned by [queue lead enrichments](queue-lead-enrichments.mdx) | ### Request samples #### Retrieve one lead enrichment batch ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments/{uuid}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def get_lead_enrichment(): url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments/{uuid}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET response:", response.json()) else: print("GET failed with status:", response.status_code, response.text) if __name__ == "__main__": get_lead_enrichment() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getLeadEnrichment(); } public static void getLeadEnrichment() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments/{uuid}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getLeadEnrichment() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments/{uuid}"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers }); if (response.status === 200) { console.log("GET response:", response.data); } else { console.error("GET request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getLeadEnrichment(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('lead_finder/leads/enrichments/{uuid}'); 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 Successfully fetched status details for a lead enrichment batch. ```json { "lead_enrichment": { "uuid": "088e2d8a-d8ac-41fc-969f-0ba4102682ac", "created": "2026-05-01T10:00:00+02:00", "leads": [ { "uid": "0SVOUNZGlkzJ5fRrBxUdiQ_0000", "status": "PROCESSED", "processing_result": "NOT_CONVERTED_TO_PROSPECT" } ] } } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `lead_enrichment` | object | One lead enrichment batch | | └─ `uuid` | string | Enrichment batch identifier | | └─ `created` | string | Batch creation timestamp in ISO 8601 format | | └─ `leads` | array[object] | Leads included in the batch | |   └─ `uid` | string | Lead identifier | |   └─ `status` | string | Processing status: `PENDING` or `PROCESSED` | |   └─ `processing_result` | string/null | Processing result: `CONVERTED_TO_PROSPECT`, `NOT_CONVERTED_TO_PROSPECT`, or `null` while pending | The batch UUID was not found for the authenticated account. A `404` can mean the enrichment batch is unavailable to this account, or that the request URL points to the wrong resource. ```json { "title": "Not Found", "status": 404, "details": "Lead enrichment with uuid 088e2d8a-d8ac-41fc-969f-0ba4102682ac was not found", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining which resource was not found | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | --- ## Get search criteria values Retrieve allowed values for a selected Lead Finder criterion. Use this endpoint for `enumerated` criteria returned by [Get search criteria](get-search-criteria.mdx), such as `COUNTRY`, `INDUSTRY`, `JOB_TITLE_ROLE`, or `COMPANY_SIZE`. This endpoint supports filtering and pagination so you can power autocomplete or searchable dropdowns in your integration before calling the lead search endpoint. Pagination metadata is returned in the response body. Use the values returned by this endpoint when building [lead search](post-search-leads.mdx) requests. Allowed values can change over time, so do not hardcode values from examples. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/lead_finder/search_criteria/{criterion_name}/values ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `criterion_name` | Yes | string | Path parameter - the criterion whose allowed values you want to list. Use a criterion name returned by [Get search criteria](get-search-criteria.mdx). Use capital letters | | `search_phrase` | No | string | Text filter applied to the value list. The search matches partial substrings, so `con` can match `construction` as well as `semiconductors`. Maximum length: 50 characters. | | `page` | No | integer | 1-based page number. Default: `1`. Minimum: `1`. | | `limit` | No | integer | Number of values to return per page. Default: `10`. Minimum: `1`. Maximum: `50`. | ### Request samples #### Retrieve past job title values ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/lead_finder/search_criteria/PAST_JOB_TITLE/values" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def get_search_criteria_values(): url = "https://api.woodpecker.co/rest/v2/lead_finder/search_criteria/PAST_JOB_TITLE/values" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": get_search_criteria_values() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getSearchCriteriaValues(); } public static void getSearchCriteriaValues() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/search_criteria/PAST_JOB_TITLE/values"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getSearchCriteriaValues() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/search_criteria/PAST_JOB_TITLE/values"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getSearchCriteriaValues(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('lead_finder/search_criteria/PAST_JOB_TITLE/values'); 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 A paginated list of matching values for the selected criterion. ```json { "criterion_name": "PAST_JOB_TITLE", "search_phrase": null, "page": 1, "limit": 10, "values": [ "owner", "manager", "president", "director", "ceo", "teacher", "project manager", "partner", "principal", "intern" ], "total_found": 4999, "last": false } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `criterion_name` | string | The criterion whose values were returned | | `search_phrase` | string/null | The trimmed search phrase used to filter the values, or `null` when no filter was used | | `page` | integer | Current 1-based page number | | `limit` | integer | Maximum number of values returned in this page | | `values` | array[string] | Matching values for the selected criterion | | `total_found` | integer | Total number of matching values across all pages | | `last` | boolean | Whether the current page is the last available page | Invalid request parameters or malformed request syntax. This can happen when `criterion_name` is not supported, `page` is lower than `1`, `limit` is lower than `1`, or `search_phrase` is longer than 50 characters. ```json { "title": "Bad Request", "status": 400, "details": "Value of {field} is incorrect.", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining which parameter was rejected | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Please review the request URL. Unsupported criterion names are returned as `400`. ```json { "title": "Not Found", "status": 404, "details": "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | --- ## Get search criteria Retrieve the current list of Lead Finder criteria you can use when building a [lead search](post-search-leads.mdx) request. The response is grouped into `freetext` criteria, where you provide your own value, and `enumerated` criteria, where you should first fetch allowed values from the [search criteria values](get-search-criteria-values.mdx) endpoint. Use this endpoint as the source of truth for supported criteria instead of hardcoding the example list from the documentation. The criteria available to your account can change over time, so build lead search requests from the current catalog returned by this endpoint. :::info For `enumerated` criteria, call [Get search criteria values](get-search-criteria-values.mdx) before building your lead search request so you can use currently supported values. ::: ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/lead_finder/search_criteria ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Retrieve the current Lead Finder criteria catalog ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/lead_finder/search_criteria" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def get_search_criteria(): url = "https://api.woodpecker.co/rest/v2/lead_finder/search_criteria" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": get_search_criteria() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getSearchCriteria(); } public static void getSearchCriteria() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/search_criteria"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getSearchCriteria() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/search_criteria"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getSearchCriteria(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('lead_finder/search_criteria'); 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 The current Lead Finder search criteria grouped by criterion type. ```json { "search_criteria": { "freetext": [ { "name": "CITY" }, { "name": "COMPANY_NAME" }, { "name": "COMPANY_WEBSITE" }, { "name": "COMPANY_CITY" }, { "name": "FIRST_NAME" }, { "name": "LAST_NAME" } ], "enumerated": [ { "name": "INDUSTRY" }, { "name": "CURRENT_JOB_TITLE" }, { "name": "PAST_JOB_TITLE" }, { "name": "COUNTRY" }, { "name": "JOB_TITLE_LEVEL" }, { "name": "JOB_TITLE_ROLE" }, { "name": "GENDER" }, { "name": "YEARS_OF_EXPERIENCE" }, { "name": "COMPANY_COUNTRY" }, { "name": "COMPANY_SIZE" }, { "name": "COMPANY_TYPE" }, { "name": "US_STATE" } ] } } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `search_criteria` | object | Container for the currently supported Lead Finder criteria | | └─ `freetext` | array[object] | Criteria where you provide your own value directly in the lead search payload | |   └─ `[].name` | string | Criterion name | | └─ `enumerated` | array[object] | Criteria where you should first fetch allowed values from the [search criteria values](get-search-criteria-values.mdx) endpoint | |   └─ `[].name` | string | Criterion name | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Please review the request URL. ```json { "title": "Not Found", "status": 404, "details": "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | --- ## List lead enrichments List lead enrichment batches requested for the authenticated account. Use this endpoint after [queueing lead enrichments](queue-lead-enrichments.mdx) to inspect requested jobs and their per-lead processing state. If you already have a specific batch `uuid`, use [get lead enrichment](get-lead-enrichment.mdx) for a direct lookup. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters By default, the endpoint returns lead enrichment batches requested during the last 24 hours. Use `created_after` to fetch batches requested after the provided cutoff date. | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `created_after` | No | string | Cutoff date in `YYYY-MM-DD` format. When provided, the endpoint returns batches requested after this date instead of using the default last-24-hours cutoff | ### Request samples #### Retrieve lead enrichment batches ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments?created_after=2026-05-01" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def get_lead_enrichments(): url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments" headers = { "x-api-key": "{YOUR_API_KEY}" } params = { "created_after": "2026-05-01" } response = requests.get(url, headers=headers, params=params) if response.status_code == 200: print("GET response:", response.json()) else: print("GET failed with status:", response.status_code, response.text) if __name__ == "__main__": get_lead_enrichments() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getLeadEnrichments(); } public static void getLeadEnrichments() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments?created_after=2026-05-01"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getLeadEnrichments() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; const params = { created_after: "2026-05-01" }; try { const response = await axios.get(url, { headers, params }); if (response.status === 200) { console.log("GET response:", response.data); } else { console.error("GET request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getLeadEnrichments(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('lead_finder/leads/enrichments', [ 'query' => [ 'created_after' => '2026-05-01', ], ]); 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 Lead enrichment batches for the authenticated account that match the selected cutoff. Use `uuid` with [get lead enrichment](get-lead-enrichment.mdx) when you need to inspect one batch directly. ```json { "lead_enrichments": [ { "uuid": "e345310f-cfa7-42e3-b2fc-d888a332a899", "created": "2026-05-01T10:00:00+02:00", "leads": [ { "uid": "0SVAICGlkzJ5fMrBxUdiQ_0000", "status": "PROCESSED", "processing_result": "NOT_CONVERTED_TO_PROSPECT" } ] }, { "uuid": "eed003ca-b107-4126-bdb6-b5638737d508", "created": "2026-05-01T10:10:00+02:00", "leads": [ { "uid": "0SVOUNZGlkzJ5fMcHsUdiQ_0000", "status": "PROCESSED", "processing_result": "NOT_CONVERTED_TO_PROSPECT" } ] } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `lead_enrichments` | array[object] | Recent lead enrichment batches | | └─ `uuid` | string | Enrichment batch identifier | | └─ `created` | string | Batch creation timestamp in ISO 8601 format | | └─ `leads` | array[object] | Leads included in the batch | |   └─ `uid` | string | Lead identifier | |   └─ `status` | string | Processing status: `PENDING` or `PROCESSED` | |   └─ `processing_result` | string/null | Processing result: `CONVERTED_TO_PROSPECT`, `NOT_CONVERTED_TO_PROSPECT`, or `null` while pending | Invalid request or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "details": "Value of created_after is incorrect.", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining which parameter was rejected | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Please review the request URL. ```json { "title": "Not Found", "status": 404, "details": "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | --- ## Lead search and enrichment Use this flow when you want to find leads who match a target audience and are not already coming from your Woodpecker prospects database. **Start by reading the Lead Finder criteria catalog**, then fetch current values for filters such as country, industry, or job title role. **Use those criteria to search for leads**. The search response returns compact lead records, including the `uid` needed for enrichment. Search criteria and allowed values can change over time, so build search requests from the current criteria endpoints. Lead profile fields also depend on data availability: when a profile value is not available, scalar fields can be returned as `null`, and array fields can be returned as an empty array. When you know which leads you want to work with, **queue them for enrichment**. Enrichment runs asynchronously: the queue response gives you a batch `uuid`, and you use the list or get endpoints to check when processing is complete. Enriched leads can be added to the global prospect list or selected campaigns with `target_campaign_ids`. :::info Credit usage Some requests in this flow use account credits. Searching for leads and viewing returned lead data costs 1 credit per lead. For enrichment requests, credits are applied only when enrichment finds data: 1.5 credits for enriching a lead and finding an email. GET endpoints used to read criteria or check enrichment status do not use credits. ::: ## Available endpoints | Endpoint | Method and path | Use it to | |----------|-----------------|-----------| | [Get search criteria](get-search-criteria.mdx) | `GET /rest/v2/lead_finder/search_criteria` | Retrieve the current criteria catalog for building lead searches | | [Get search criteria values](get-search-criteria-values.mdx) | `GET /rest/v2/lead_finder/search_criteria/{criterion_name}/values` | Retrieve allowed values for enumerated criteria | | [Search leads](post-search-leads.mdx) | `POST /rest/v2/lead_finder/leads` | Find people who match your selected criteria | | [Queue lead enrichments](queue-lead-enrichments.mdx) | `POST /rest/v2/lead_finder/leads/enrichments` | Start an asynchronous enrichment batch for selected lead search results | | [List lead enrichments](list-lead-enrichments.mdx) | `GET /rest/v2/lead_finder/leads/enrichments` | List recent lead enrichment batches and processing statuses | | [Get lead enrichment](get-lead-enrichment.mdx) | `GET /rest/v2/lead_finder/leads/enrichments/{uuid}` | Retrieve one lead enrichment batch by its returned `uuid` | --- ## Search leads Search for people who match one or more Lead Finder criteria. This endpoint returns a compact result set intended for discovery, filtering, and selecting leads for further enrichment. Use the returned lead `uid` and other lead details when queueing a lead for enrichment with the [queue lead enrichments](queue-lead-enrichments.mdx) endpoint. ## Request Each request must include a non-empty `search_criteria` array. Every criterion contains a `name`, a `value`, and an `operator`, where `INCLUDE` narrows the search to matching values and `EXCLUDE` removes matching values from the result set. To match multiple values for the same criterion, add multiple `search_criteria` objects with the same name. Build requests from the current [search criteria](get-search-criteria.mdx) catalog. For enumerated criteria such as `COUNTRY` or `INDUSTRY`, fetch current allowed values from [search criteria values](get-search-criteria-values.mdx) before building the request. Criteria and allowed values can change over time, so do not hardcode the example values from this page. :::info Credit usage This request can use account credits. Searching for leads and viewing returned lead data costs 1 credit per lead. ::: ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/lead_finder/leads ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "search_criteria": [ { "name": "INDUSTRY", "value": "banking", "operator": "INCLUDE" }, { "name": "CURRENT_JOB_TITLE", "value": "ceo", "operator": "INCLUDE" }, { "name": "COUNTRY", "value": "poland", "operator": "EXCLUDE" } ], "size": 2, "next_page": null } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `search_criteria` | array[object] | Yes | Non-empty list of filters to apply | |   └─ `name` | string | Yes | Criterion name from the [search criteria](get-search-criteria.mdx) catalog. Case-sensitive. | |   └─ `value` | string | Yes | Criterion value. For enumerated criteria (case-sensitive), fetch allowed values from [search criteria values](get-search-criteria-values.mdx). | |   └─ `operator` | string | Yes | Filter behavior: `INCLUDE` keeps matches, `EXCLUDE` removes matches | | `size` | integer | No | Number of leads to return. Default: `1`. Maximum: `50` for PREMIUM accounts and `25` for TRIAL accounts | | `next_page` | string | No | Pagination token returned by a previous search response. Available only for PREMIUM accounts | ### Request samples #### Search for leads with inclusion and exclusion filters ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/lead_finder/leads" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "search_criteria": [ { "name": "INDUSTRY", "value": "banking", "operator": "INCLUDE" }, { "name": "COUNTRY", "value": "poland", "operator": "EXCLUDE" }, { "name": "CURRENT_JOB_TITLE", "value": "ceo", "operator": "INCLUDE" } ], "size": 2, "next_page": null }' ``` ```python import requests def search_leads(): url = "https://api.woodpecker.co/rest/v2/lead_finder/leads" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "search_criteria": [ { "name": "INDUSTRY", "value": "banking", "operator": "INCLUDE" }, { "name": "COUNTRY", "value": "poland", "operator": "EXCLUDE" }, { "name": "CURRENT_JOB_TITLE", "value": "ceo", "operator": "INCLUDE" } ], "size": 2, "next_page": null } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST response:", response.json()) else: print("POST failed with status:", response.status_code, response.text) if __name__ == "__main__": search_leads() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { searchLeads(); } public static void searchLeads() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/leads"; String jsonData = """ { "search_criteria": [ { "name": "INDUSTRY", "value": "banking", "operator": "INCLUDE" }, { "name": "COUNTRY", "value": "poland", "operator": "EXCLUDE" }, { "name": "CURRENT_JOB_TITLE", "value": "ceo", "operator": "INCLUDE" } ], "size": 2, "next_page": null } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function searchLeads() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/leads"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { search_criteria: [ { name: "INDUSTRY", value: "banking", operator: "INCLUDE" }, { name: "COUNTRY", value: "poland", operator: "EXCLUDE" }, { name: "CURRENT_JOB_TITLE", value: "ceo", operator: "INCLUDE" } ], size: 2, next_page: null }; try { const response = await axios.post(url, data, { headers }); if (response.status === 200) { console.log("POST response:", response.data); } else { console.error("POST request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } searchLeads(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('lead_finder/leads', [ 'json' => [ 'search_criteria' => [ [ 'name' => 'INDUSTRY', 'value' => 'banking', 'operator' => 'INCLUDE', ], [ 'name' => 'COUNTRY', 'value' => 'poland', 'operator' => 'EXCLUDE', ], [ 'name' => 'CURRENT_JOB_TITLE', 'value' => 'ceo', 'operator' => 'INCLUDE', ], ], 'size' => 2, 'next_page' => null ], ]); 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 Preview of the leads matching the search criteria ```json { "size": 2, "leads": [ { "uid": "0SVOUN5zYlkzJ5fMrBxUdiQ_0000", "full_name": "Erlich Bachman", "first_name": "Erlich", "last_name": "Bachman", "gender": "Male", "linkedin_url": "linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "bachmanity.com", "industry": "Software as a Service", "job_title": "Ceo", "job_title_role": "Operations", "job_title_levels": [ "Cxo" ], "location_name": "Palo Alto, California, United States", "city": "Palo Alto", "state": "California", "country": "United States" }, { "uid": "4fl-ZIc98WqIFbOFgcRzkw_0000", "full_name": "Jared Dunn", "first_name": "Jared", "last_name": "Dunn", "gender": null, "linkedin_url": null, "company_name": "Pied Piper", "company_website": "piedpiper.com", "industry": "Software as a Service", "job_title": "Ceo", "job_title_role": null, "job_title_levels": [], "location_name": "Palo Alto, California, United States", "city": "Palo Alto", "state": null, "country": "United States" } ], "total_found": 28639, "next_page": "206$8.458225" } ``` Profile fields depend on data availability. When a scalar profile value is not available, it is returned as `null`. When an array profile value is not available, it is returned as an empty array. #### Body schema | Field | Type | Description | |-----------------------------------|----------------|----------------------------------------------------------| | `size` | integer | Number of leads requested in this page | | `leads` | array[object] | Lead search result items | |   └─ `uid` | string | Lead identifier | |   └─ `full_name` | string/null | Full name of the lead | |   └─ `first_name` | string/null | First name | |   └─ `last_name` | string/null | Last name | |   └─ `gender` | string/null | Gender | |   └─ `linkedin_url` | string/null | LinkedIn profile URL without a http prefix | |   └─ `company_name` | string/null | Current company name | |   └─ `company_website` | string/null | Current company website | |   └─ `industry` | string/null | Current industry | |   └─ `job_title` | string/null | Current job title | |   └─ `job_title_role` | string/null | Job title role when available | |   └─ `job_title_levels` | array[string] | Job seniority levels. Empty when not available | |   └─ `location_name` | string/null | Full human-readable location | |   └─ `city` | string/null | City | |   └─ `state` | string/null | State or region | |   └─ `country` | string/null | Country | | `total_found` | integer | Total number of matches available for the current search | | `next_page` | string | Pagination cursor. Use to request the next page of results | Invalid search criteria, unsupported pagination usage, or another business validation error. ```json { "title": "Bad Request", "status": 400, "details": "Value of next_page is incorrect: must be null.", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the validation or request problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Not enough credits. An account admin can manage credits in the billing section of the app. ```json { "title": "Payment Required", "status": 402, "details": "Insufficient credits to find leads", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the payment or credits problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | No leads matching your search criteria were found. ```json { "title": "Not Found", "status": 404, "details": "No records were found matching your search", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | ### Pagination This endpoint uses cursor-based pagination. The response includes a `next_page` token when another page is available. Store the token and resend it exactly as returned in the next request body. Fetching additional pages is available only for PREMIUM accounts. TRIAL accounts can retrieve only the first page of results. Requests for additional pages from TRIAL accounts return 400 Bad Request. **Request first page:** ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/lead_finder/leads" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "search_criteria": [ { "name": "INDUSTRY", "value": "banking", "operator": "INCLUDE" } ], "size": 2 }' ``` Example response excerpt: ```json { "size": 2, "leads": [ { "uid": "0SVOUNZGlkzJ5fMrBxUdiQ_0000", "full_name": "Erlich Bachman" } ], "total_found": 28639, "next_page": "206$8.458299" } ``` **Request the next page:** ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/lead_finder/leads" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "search_criteria": [ { "name": "INDUSTRY", "value": "banking", "operator": "INCLUDE" } ], "size": 2, "next_page": "206$8.458299" }' ``` --- ## Queue lead enrichments Queue one or more Lead Finder search results for enrichment. This endpoint starts an asynchronous lead enrichment batch and returns a batch `uuid` that you can later check with [list lead enrichments](list-lead-enrichments.mdx) or [get lead enrichment](get-lead-enrichment.mdx). ## Request :::info Credit usage This request can use account credits. Credits are applied only when enrichment finds data. Enriching a lead and finding an email costs 1.5 credits. ::: ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body You can enrich up to 50 leads in one request. Pass the lead context returned by [lead search](post-search-leads.mdx) whenever possible, not only `uid`, because enrichment usually needs signals such as `linkedin_url` or `full_name` with `company_website`. We recommend passing through the whole lead payload with all values that are available. If lead search does not return a value, you can omit that optional field or pass the returned `null` or empty array value. ```json { "leads": [ { "uid": "0SVOUN5zYlkzJ5fMrBxUdiQ_0000", "full_name": "Erlich Bachman", "first_name": "Erlich", "last_name": "Bachman", "gender": "Male", "linkedin_url": "linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "bachmanity.com", "industry": "Software as a Service", "job_title": "Ceo", "job_title_role": "Operations", "job_title_levels": [ "Cxo" ], "location_name": "Palo Alto, California, United States", "city": "Palo Alto", "state": "California", "country": "United States" } ], "target_campaign_ids": [123,654] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `leads` | array[object] | Yes | Non-empty list of leads to enrich | |   └─ `uid` | string | Yes | Lead identifier returned by the [lead search](post-search-leads.mdx) response | |   └─ `full_name` | string/null | No | Full lead name | |   └─ `first_name` | string/null | No | Lead first name | |   └─ `last_name` | string/null | No | Lead last name | |   └─ `gender` | string/null | No | Lead gender when available | |   └─ `linkedin_url` | string/null | No | Lead LinkedIn profile URL | |   └─ `company_name` | string/null | No | Company name | |   └─ `company_website` | string/null | No | Company website | |   └─ `industry` | string/null | No | Industry name | |   └─ `job_title` | string/null | No | Current job title | |   └─ `job_title_role` | string/null | No | Job title role | |   └─ `job_title_levels` | array[string] | No | Job title levels | |   └─ `location_name` | string/null | No | Full human-readable location | |   └─ `city` | string/null | No | City | |   └─ `state` | string/null | No | State or region | |   └─ `country` | string/null | No | Country | | `target_campaign_ids` | array[integer] | No | IDs of campaigns where enriched leads should be added as prospects. Use an empty array to add them only to the global prospect list | ### Request samples #### Queue leads for enrichment ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "leads": [ { "uid": "0SVOUN5zYlkzJ5fMrBxUdiQ_0000", "full_name": "Erlich Bachman", "first_name": "Erlich", "last_name": "Bachman", "gender": "Male", "linkedin_url": "linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "bachmanity.com", "industry": "Software as a Service", "job_title": "Ceo", "job_title_role": "Operations", "job_title_levels": ["Cxo"], "location_name": "Palo Alto, California, United States", "city": "Palo Alto", "state": "California", "country": "United States" } ], "target_campaign_ids": [] }' ``` ```python import requests def queue_lead_enrichments(): url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "leads": [ { "uid": "0SVOUN5zYlkzJ5fMrBxUdiQ_0000", "full_name": "Erlich Bachman", "first_name": "Erlich", "last_name": "Bachman", "gender": "Male", "linkedin_url": "linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "bachmanity.com", "industry": "Software as a Service", "job_title": "Ceo", "job_title_role": "Operations", "job_title_levels": ["Cxo"], "location_name": "Palo Alto, California, United States", "city": "Palo Alto", "state": "California", "country": "United States" } ], "target_campaign_ids": [] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST response:", response.json()) else: print("POST failed with status:", response.status_code, response.text) if __name__ == "__main__": queue_lead_enrichments() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { queueLeadEnrichments(); } public static void queueLeadEnrichments() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments"; String jsonData = """ { "leads": [ { "uid": "0SVOUN5zYlkzJ5fMrBxUdiQ_0000", "full_name": "Erlich Bachman", "first_name": "Erlich", "last_name": "Bachman", "gender": "Male", "linkedin_url": "linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "bachmanity.com", "industry": "Software as a Service", "job_title": "Ceo", "job_title_role": "Operations", "job_title_levels": ["Cxo"], "location_name": "Palo Alto, California, United States", "city": "Palo Alto", "state": "California", "country": "United States" } ], "target_campaign_ids": [] } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function queueLeadEnrichments() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/leads/enrichments"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { leads: [ { uid: "0SVOUN5zYlkzJ5fMrBxUdiQ_0000", full_name: "Erlich Bachman", first_name: "Erlich", last_name: "Bachman", gender: "Male", linkedin_url: "linkedin.com/in/erlich-bachman-404xyz", company_name: "Bachmanity", company_website: "bachmanity.com", industry: "Software as a Service", job_title: "Ceo", job_title_role: "Operations", job_title_levels: ["Cxo"], location_name: "Palo Alto, California, United States", city: "Palo Alto", state: "California", country: "United States" } ], target_campaign_ids: [] }; try { const response = await axios.post(url, data, { headers }); if (response.status === 200) { console.log("POST response:", response.data); } else { console.error("POST request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } queueLeadEnrichments(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('lead_finder/leads/enrichments', [ 'json' => [ 'leads' => [ [ 'uid' => '0SVOUN5zYlkzJ5fMrBxUdiQ_0000', 'full_name' => 'Erlich Bachman', 'first_name' => 'Erlich', 'last_name' => 'Bachman', 'gender' => 'Male', 'linkedin_url' => 'linkedin.com/in/erlich-bachman-404xyz', 'company_name' => 'Bachmanity', 'company_website' => 'bachmanity.com', 'industry' => 'Software as a Service', 'job_title' => 'Ceo', 'job_title_role' => 'Operations', 'job_title_levels' => ['Cxo'], 'location_name' => 'Palo Alto, California, United States', 'city' => 'Palo Alto', 'state' => 'California', 'country' => 'United States', ], ], 'target_campaign_ids' => [], ], ]); 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 Batch accepted for processing. Use [get lead enrichment](get-lead-enrichment.mdx) to check the status. Use [list lead enrichments](list-lead-enrichments.mdx) to browse recent batches. ```json { "uuid": "4480b1e0-70d2-4b4a-be49-6bf9637bf0bf", "leads_count": 5, "status": "ENQUEUED" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `uuid` | string/null | Enrichment batch identifier used for polling | | `leads_count` | integer | Number of leads queued in this response | | `status` | string | Queue status. `ENQUEUED` - at least one lead was accepted and all eligible leads were queued `PARTIALLY_ENQUEUED` - some leads were queued. Usually because available credits can cover fewer leads than requested `NOT_ENQUEUED` - none of the submitted leads were queued because they are already pending `CREDITS_EXHAUSTED` - Not enough credits. An account admin can manage credits in the billing section of the app | Invalid request or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "details": "string", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the validation or request problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Please review the request URL. ```json { "title": "Not Found", "status": 404, "details": "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | --- ## Get prospect enrichment Retrieve status details for a prospect enrichment batch. Use this endpoint after [queueing prospect enrichments](queue-prospect-enrichments.mdx) when you already know the batch ID and want the most direct lookup. Use this endpoint to retrieve prospect enrichment statuses for a single batch. To retrieve statuses across multiple batches, use [list prospect enrichments](list-prospect-enrichments.mdx). ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/{uuid} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `uuid` | Yes | string | Enrichment batch UUID returned by [queue prospect enrichments](queue-prospect-enrichments.mdx) | ### Request samples #### Retrieve one prospect enrichment batch ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/{uuid}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def get_prospect_enrichment(): url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/{uuid}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET response:", response.json()) else: print("GET failed with status:", response.status_code, response.text) if __name__ == "__main__": get_prospect_enrichment() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getProspectEnrichment(); } public static void getProspectEnrichment() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/{uuid}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getProspectEnrichment() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/{uuid}"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers }); if (response.status === 200) { console.log("GET response:", response.data); } else { console.error("GET request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getProspectEnrichment(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('lead_finder/prospects/enrichments/{uuid}'); 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 Prospect enrichment fetched successfully. ```json { "prospect_enrichments": [ { "uuid": "e9e6abc4-729e-4c22-b412-8917b1db919d", "prospect_email": "erlich@bachmanity.com", "created": "2026-05-01T10:00:00+02:00", "status": "PROCESSED_ENRICHED" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `prospect_enrichments` | array[object] | Prospect enrichment status records for the requested batch | |   └─ `uuid` | string | Enrichment batch identifier | |   └─ `prospect_email` | string | Prospect email address | |   └─ `created` | string | Record creation timestamp in ISO 8601 format | |   └─ `status` | string | Processing status: `PENDING`, `PROCESSED_ENRICHED`, or `PROCESSED_NOT_ENRICHED` | The batch UUID was not found for the authenticated account. A `404` can mean the enrichment batch is unavailable to this account, or that the request URL points to the wrong resource. ```json { "title": "Not Found", "status": 404, "details": "Prospect enrichment with uuid 65d58771-7da2-44ff-b891-5e8484393635 was not found", "timestamp": "2026-05-01 10:00:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining which resource was not found | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "title": "Unauthorized", "status": 401, "detail": "Invalid api key", "timestamp": "2026-05-01 10:00: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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | --- ## List prospect enrichments List prospect enrichment statuses for the authenticated account. Use this endpoint after [queueing prospect enrichments](queue-prospect-enrichments.mdx) to inspect recent enrichment jobs and their per-prospect processing state. With an empty request, this endpoint returns recent prospect enrichment statuses. With an `emails` filter, it returns statuses for the selected prospects. If you already know the batch `uuid`, use [get prospect enrichment](get-prospect-enrichment.mdx) for a direct lookup. ## Request ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/statuses/query ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body To retrieve recent prospect enrichment statuses, send an empty body. To retrieve statuses for selected prospects, send `emails`: ```json { "emails": [ "erlich@bachmanity.com" ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `emails` | array[string] | No | Optional list of prospect emails to filter by | ### Request samples #### Query statuses for selected prospect emails ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/statuses/query" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "emails": [ "erlich@bachmanity.com" ] }' ``` ```python import requests def query_prospect_enrichment_statuses(): url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/statuses/query" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "emails": [ "erlich@bachmanity.com" ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST response:", response.json()) else: print("POST failed with status:", response.status_code, response.text) if __name__ == "__main__": query_prospect_enrichment_statuses() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { queryProspectEnrichmentStatuses(); } public static void queryProspectEnrichmentStatuses() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/statuses/query"; String jsonData = """ { "emails": [ "erlich@bachmanity.com" ] } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function queryProspectEnrichmentStatuses() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments/statuses/query"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { emails: [ "erlich@bachmanity.com" ] }; try { const response = await axios.post(url, data, { headers }); if (response.status === 200) { console.log("POST response:", response.data); } else { console.error("POST request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } queryProspectEnrichmentStatuses(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('lead_finder/prospects/enrichments/statuses/query', [ 'json' => [ 'emails' => [ 'erlich@bachmanity.com', ], ], ]); 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 Prospect enrichment statuses for the authenticated account. Use `uuid` with [get prospect enrichment](get-prospect-enrichment.mdx) when you need to inspect one batch directly. ```json { "prospect_enrichments": [ { "uuid": "e9e6abc4-799e-4c22-b412-7917b1db919d", "prospect_email": "erlich@bachmanity.com", "created": "2026-05-15T11:14:56+02:00", "status": "PROCESSED_NOT_ENRICHED" }, { "uuid": "e9e6abc4-799e-4c22-b412-7917b1db919d", "prospect_email": "jared@piedpiper.com", "created": "2026-05-15T11:14:56+02:00", "status": "PROCESSED_ENRICHED" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `prospect_enrichments` | array[object] | Prospect enrichment status records | |   └─ `uuid` | string | Enrichment batch identifier | |   └─ `prospect_email` | string | Prospect email address | |   └─ `created` | string | Record creation timestamp in ISO 8601 format | |   └─ `status` | string | Processing status. `PENDING` - enrichment is queued or still being processed `PROCESSED_ENRICHED` - enrichment finished and found additional prospect data `PROCESSED_NOT_ENRICHED` - enrichment finished but did not find additional prospect data | Invalid request or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "details": "string", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the validation or request problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Please review the request URL. ```json { "title": "Not Found", "status": 404, "details": "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | --- ## Prospect enrichment Use this flow when you **already have prospects in Woodpecker and want to enrich those existing records** with additional data. If you need to find or manage those records first, use the [Prospects](/docs/prospects/prospects.mdx) endpoints. **Queue prospects by email** and include as much useful context as you have, such as first name, last name, LinkedIn URL, company name, or company website. The submitted email should belong to an existing prospect in the account's main prospect database. Prospect enrichment is asynchronous. The queue endpoint returns a batch `uuid` and queue status. Use the status query endpoint for recent or email-filtered results, or fetch a specific batch by `uuid` when you want to inspect one job directly. :::info Credit usage Some requests in this flow use account credits. For enrichment requests, credits are applied only when enrichment finds data: 1.5 credits for successfully enriching an existing prospect. GET endpoints used to check enrichment status do not use credits. ::: ## Available endpoints | Endpoint | Method and path | Use it to | |----------|-----------------|-----------| | [Queue prospect enrichments](queue-prospect-enrichments.mdx) | `POST /rest/v2/lead_finder/prospects/enrichments` | Start an asynchronous enrichment batch for existing prospects. | | [List prospect enrichments](list-prospect-enrichments.mdx) | `POST /rest/v2/lead_finder/prospects/enrichments/statuses/query` | Query recent prospect enrichment statuses or filter them by email. | | [Get prospect enrichment](get-prospect-enrichment.mdx) | `GET /rest/v2/lead_finder/prospects/enrichments/{uuid}` | Retrieve one prospect enrichment batch by its returned `uuid`. | --- ## Queue prospect enrichments Queue one or more prospects for enrichment. This endpoint starts an asynchronous prospect enrichment batch and returns a batch `uuid` that you can later check with [list prospect enrichments](list-prospect-enrichments.mdx) or [get prospect enrichment](get-prospect-enrichment.mdx). Use this flow to enrich prospects that already exist in your account's main prospect database. The endpoint matches submitted records by email and updates matching prospects after enrichment; it does not create new prospects or update records that are not already in that database. ## Request :::info Credit usage This request can use account credits. Credits are applied only when enrichment finds data. Successfully enriching an existing prospect costs 1.5 credits. ::: ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body Provide wide prospect context whenever possible. Enrichment usually needs additional signals, such as `linkedin_url` or `first_name` and `last_name` together with `company_website`. Each submitted `email` should belong to an existing prospect in your account's main prospect database. ```json { "prospects": [ { "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "https://bachmanity.com" }, { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company_name": "Pied Piper", "company_website": "https://piedpiper.com" } ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `prospects` | array[object] | Yes | Non-empty list of prospects to enrich | |   └─ `email` | string | Yes | Email address of an existing prospect in the account's main prospect database. The endpoint uses this value as the enrichment identity | |   └─ `first_name` | string | No | Prospect's first name | |   └─ `last_name` | string | No | Prospect's last name | |   └─ `linkedin_url` | string | No | Prospect's LinkedIn profile URL | |   └─ `company_name` | string | No | Prospect's company name | |   └─ `company_website` | string | No | Prospect's company website | ### Request samples #### Queue prospects for enrichment ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "prospects": [ { "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "https://bachmanity.com" }, { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company_name": "Pied Piper", "company_website": "https://piedpiper.com" } ] }' ``` ```python import requests def queue_prospect_enrichments(): url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "prospects": [ { "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "https://bachmanity.com" }, { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company_name": "Pied Piper", "company_website": "https://piedpiper.com" } ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST response:", response.json()) else: print("POST failed with status:", response.status_code, response.text) if __name__ == "__main__": queue_prospect_enrichments() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { queueProspectEnrichments(); } public static void queueProspectEnrichments() { try { String url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments"; String jsonData = """ { "prospects": [ { "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz", "company_name": "Bachmanity", "company_website": "https://bachmanity.com" }, { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company_name": "Pied Piper", "company_website": "https://piedpiper.com" } ] } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function queueProspectEnrichments() { const url = "https://api.woodpecker.co/rest/v2/lead_finder/prospects/enrichments"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { prospects: [ { email: "erlich@bachmanity.com", first_name: "Erlich", last_name: "Bachman", linkedin_url: "https://www.linkedin.com/in/erlich-bachman-404xyz", company_name: "Bachmanity", company_website: "https://bachmanity.com" }, { email: "jared@piedpiper.com", first_name: "Jared", last_name: "Dunn", company_name: "Pied Piper", company_website: "https://piedpiper.com" } ] }; try { const response = await axios.post(url, data, { headers }); if (response.status === 200) { console.log("POST response:", response.data); } else { console.error("POST request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } queueProspectEnrichments(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('lead_finder/prospects/enrichments', [ 'json' => [ 'prospects' => [ [ 'email' => 'erlich@bachmanity.com', 'first_name' => 'Erlich', 'last_name' => 'Bachman', 'linkedin_url' => 'https://www.linkedin.com/in/erlich-bachman-404xyz', 'company_name' => 'Bachmanity', 'company_website' => 'https://bachmanity.com', ], [ 'email' => 'jared@piedpiper.com', 'first_name' => 'Jared', 'last_name' => 'Dunn', 'company_name' => 'Pied Piper', 'company_website' => 'https://piedpiper.com', ], ], ], ]); 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 Batch accepted for processing. Use [list prospect enrichments](list-prospect-enrichments.mdx) to check recent statuses. Use [get prospect enrichment](get-prospect-enrichment.mdx) when you already know the returned `uuid`. ```json { "uuid": "e9e6abc4-799e-4c24-b412-7917b1db919d", "prospects_count": 2, "total_prospects_count": 2, "status": "ENQUEUED" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `uuid` | string/null | Enrichment batch identifier. `null` when nothing was queued | | `prospects_count` | integer | Number of prospects queued in this response | | `total_prospects_count` | integer | Number of prospects submitted in the request | | `status` | string | Queue status. `ENQUEUED` - at least one prospect was accepted and all eligible prospects were queued `PARTIALLY_ENQUEUED` - some prospects were queued. Usually because available credits can cover fewer prospects than requested `NOT_ENQUEUED` - none of the submitted prospects were queued because they are already pending `CREDITS_EXHAUSTED` - not enough credits. An account admin can manage credits in the billing section of the app | Invalid request or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "details": "string", "timestamp": "2025-03-05 17:57:00", "extra": null } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the validation or request problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | | `extra` | string/null | Additional information about the error, when available | An issue with authorization. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "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 authorization problem | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Please review the request URL. ```json { "title": "Not Found", "status": 404, "details": "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | Unexpected error, please try again later. ```json { "title": "Internal server error", "status": 500, "details": "An unexpected error has occurred. Please try again later.", "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 | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred, `YYYY-MM-DD HH:MM:SS` UTC | --- ## Get a collection job Retrieve a collection job by the UUID returned when you [collect profiles from posts](post-collect-profiles.mdx). Use this endpoint to monitor a known collector directly. To browse recent collectors, use [list collection jobs](get-collection-jobs.mdx). Polling this endpoint does not use credits. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/{uid} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `uid` | Yes | string | Collector UUID returned by [collect profiles from posts](post-collect-profiles.mdx) | ### Request samples #### Retrieve a collector ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/e9e6abc4-799e-4c24-b412-7917b1db918c" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_post_profile_collector(collector_uid): url = f"https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/{collector_uid}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: print( "GET response:", get_post_profile_collector("e9e6abc4-799e-4c24-b412-7917b1db918c") ) except Exception as error: print("Error:", error) ``` ```java 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) { getPostProfileCollector("e9e6abc4-799e-4c24-b412-7917b1db918c"); } private static void getPostProfileCollector(String collectorUid) { try { String url = "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/" + collectorUid; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new RuntimeException( "GET request failed: " + response.statusCode() + ", " + response.body() ); } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require("axios"); async function getPostProfileCollector(collectorUid) { const url = `https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/${collectorUid}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers }); console.log("GET response:", response.data); } catch (error) { console.error( "GET request failed:", error.response ? error.response.status : error.message ); } } getPostProfileCollector("e9e6abc4-799e-4c24-b412-7917b1db918c"); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $collectorUid = 'e9e6abc4-799e-4c24-b412-7917b1db918c'; try { $response = $client->get('linkedin/post_profiles_collectors/' . $collectorUid); 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 The requested collector details ```json { "uid": "e9e6abc4-799e-4c24-b412-7917b1db918c", "post_url": "https://www.linkedin.com/posts/linkedin_post_url", "target_list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1", "status": "SUCCESS", "collect_from": "REACTIONS", "collected_profiles_count": 27, "created": "2026-08-05T10:15:30+02:00" } ``` :::info Retrying collection If a collection is interrupted and saves partial results, you can submit the post for collection again. The new job starts from the beginning, so credits are charged again for every profile it collects, including profiles saved by the previous job ::: #### Body schema | Field | Type | Description | |-------|------|-------------| | `uid` | string | Collector UUID | | `post_url` | string | LinkedIn post processed by the collector | | `target_list_uid` | string | UUID of the list receiving collected data | | `status` | string | Collector status: `COLLECTING` - collection is in progress`SUCCESS` - all available results were collected successfully`INVALID_URL` - the URL passed the initial format validation but could not be processed as a valid LinkedIn post. Check the URL and submit a new request`INTERRUPTED` - collection stopped after saving a partial result. Submit a new request to try collecting the remaining profiles`INSUFFICIENT_CREDITS` - collection could not start and no profiles were saved. The account did not have enough credits. [Review your billing](https://app.woodpecker.co/panel#settings/billing/subscription-plan) and try again`INTERRUPTED_INSUFFICIENT_CREDITS` - some profiles were saved before the account ran out of credits. [Review your billing](https://app.woodpecker.co/panel#settings/billing/subscription-plan) and submit a new request`ERROR` - collection failed before any profiles could be saved. Try again, and contact support if the issue persists | | `collect_from` | string | Collection source: `REACTIONS` or `COMMENTS` | | `collected_profiles_count` | integer | Number of collected profiles saved to the target list | | `created` | string | Collector creation time in ISO 8601 format, including an offset | `uid` is not a valid UUID ```json { "type": "validation_error", "code": "invalid_fields", "message": "Invalid field(s)", "requestId": "4a3cc945-f502-4f83-b0ee-5d7fd1f38548", "fields": [ { "field": "uid", "issue": "must_be_valid_uuid", "value": "not-a-uuid" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `validation_error` | | `code` | string | Error code: `invalid_fields` | | `message` | string | Human-readable error summary | | `requestId` | string/null | Request identifier, or `null` when unavailable | | `fields` | array[object] | Invalid path parameters | |   └─ `field` | string | Invalid field: `uid` | |   └─ `issue` | string | Validation issue: `must_be_valid_uuid` | |   └─ `value` | string | Rejected path value | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "Invalid api key", "timestamp": "2026-08-05 10:00: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 | No collector with this UUID exists for the authenticated Woodpecker account ```text Status: 404 Body: none ``` Unexpected error, please try again later ```json { "type": "internal_error", "code": "internal_server_error", "message": "An unexpected error occurred on the server.", "requestId": "4a3cc945-f502-4f83-b0ee-5d7fd1f38548" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `internal_error` | | `code` | string | Error code: `internal_server_error` | | `message` | string | Human-readable error summary | | `requestId` | string/null | Request identifier, or `null` when unavailable | --- ## List collection jobs Retrieve collection jobs for the authenticated Woodpecker account. Use this endpoint after [collecting profiles from posts](post-collect-profiles.mdx) to monitor their statuses or locate their collector and target list UUIDs. Results are ordered from newest to oldest and returned in fixed pages of 100. Polling this endpoint does not use credits. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors?page=1 ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Default | Description | |-----------|:--------:|------|:-------:|-------------| | `page` | No | integer | `1` | One-based page number | ### Request samples #### List the first page of collectors ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors?page=1" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def list_post_profile_collectors(): url = "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors" headers = { "x-api-key": "{YOUR_API_KEY}" } params = { "page": 1 } response = requests.get(url, headers=headers, params=params) if response.status_code == 200: return response.json() raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: print("GET response:", list_post_profile_collectors()) except Exception as error: print("Error:", error) ``` ```java 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/linkedin/post_profiles_collectors?page=1"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new RuntimeException( "GET request failed: " + response.statusCode() + ", " + response.body() ); } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require("axios"); async function listPostProfileCollectors() { const url = "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; const params = { page: 1 }; try { const response = await axios.get(url, { headers, params }); console.log("GET response:", response.data); } catch (error) { console.error( "GET request failed:", error.response ? error.response.status : error.message ); } } listPostProfileCollectors(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('linkedin/post_profiles_collectors', [ 'query' => [ 'page' => 1, ], ]); 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 Collectors for the requested page. `post_collectors` is an empty array when the account has no collectors on that page. ```json { "post_collectors": [ { "uid": "caf15107-53dc-4f19-8920-ddfbd98b44c3", "post_url": "https://www.linkedin.com/posts/linkedin_post_url", "target_list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1", "status": "COLLECTING", "collect_from": "COMMENTS", "collected_profiles_count": 0, "created": "2026-08-05T10:15:30+02:00" }, { "uid": "e9e6abc4-799e-4c24-b412-7917b1db919d", "post_url": "https://www.linkedin.com/posts/linkedin_post_url", "target_list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1", "status": "SUCCESS", "collect_from": "REACTIONS", "collected_profiles_count": 27, "created": "2026-08-05T10:15:30+02:00" } ], "pagination_data": { "total_elements": 2, "total_pages": 1, "current_page_number": 1, "page_size": 100 } } ``` :::info Retrying collection If a collection is interrupted and saves partial results, you can submit the post for collection again. The new job starts from the beginning, so credits are charged again for every profile it collects, including profiles saved by the previous job ::: #### Body schema | Field | Type | Description | |-------|------|-------------| | `post_collectors` | array[object] | Collectors on the requested page, ordered from newest to oldest | |   └─ `uid` | string | Collector UUID | |   └─ `post_url` | string | LinkedIn post processed by the collector | |   └─ `target_list_uid` | string | UUID of the list receiving collected data | |   └─ `status` | string | Collector status: `COLLECTING` - collection is in progress`SUCCESS` - all available results were collected successfully`INVALID_URL` - the URL passed the initial format validation but could not be processed as a valid LinkedIn post. Check the URL and submit a new request`INTERRUPTED` - collection stopped after saving a partial result. Submit a new request to try collecting the remaining profiles`INSUFFICIENT_CREDITS` - collection could not start and no profiles were saved. The account did not have enough credits. [Review your billing](https://app.woodpecker.co/panel#settings/billing/subscription-plan) and try again`INTERRUPTED_INSUFFICIENT_CREDITS` - some profiles were saved before the account ran out of credits. [Review your billing](https://app.woodpecker.co/panel#settings/billing/subscription-plan) and submit a new request`ERROR` - collection failed before any profiles could be saved. Try again, and contact support if the issue persists | |   └─ `collect_from` | string | Collection source: `REACTIONS` or `COMMENTS` | |   └─ `collected_profiles_count` | integer | Number of collected profiles saved to the target list | |   └─ `created` | string | Collector creation time in ISO 8601 format, including an offset | | `pagination_data` | object | Pagination metadata | |   └─ `total_elements` | integer | Total number of collectors in the account | |   └─ `total_pages` | integer | Total number of pages at 100 collectors per page | |   └─ `current_page_number` | integer | Requested one-based page number | |   └─ `page_size` | integer | Fixed page size: `100` | `page` is not an integer greater than zero. ```json { "type": "validation_error", "code": "invalid_fields", "message": "Invalid field(s)", "requestId": "4a3cc945-f502-4f83-b0ee-5d7fd1f38548", "fields": [ { "field": "page", "issue": "must_be_greater_than_zero", "value": 0 } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `validation_error` | | `code` | string | Error code: `invalid_fields` | | `message` | string | Human-readable error summary | | `requestId` | string/null | Request identifier, or `null` when unavailable | | `fields` | array[object] | Invalid parameters | |   └─ `field` | string | Invalid field: `page` | |   └─ `issue` | string | Validation issue: `must_be_greater_than_zero` | |   └─ `value` | integer/string | Rejected query value | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "Invalid api key", "timestamp": "2026-08-05 10:00: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 | Unexpected error, please try again later ```json { "type": "internal_error", "code": "internal_server_error", "message": "An unexpected error occurred on the server.", "requestId": "4a3cc945-f502-4f83-b0ee-5d7fd1f38548" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `internal_error` | | `code` | string | Error code: `internal_server_error` | | `message` | string | Human-readable error summary | | `requestId` | string/null | Request identifier, or `null` when unavailable | --- ## Get enrichment status Use this endpoint to check the status of an enrichment job started by [enrichment of collected profiles](post-enrich-profiles.mdx). Pass the `action_uid` returned by that request to retrieve the current processing state and its result summary. ## Request Use the `action_uid` returned when you enqueue the enrichment. ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/{action_uid} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `action_uid` | Yes | string | Enrichment action UUID returned by [enrich collected profiles](post-enrich-profiles.mdx) | ### Request samples #### Retrieve an enrichment action ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/89cb9618-945f-40f2-a147-86fd508933c2" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_post_profile_enrichment_status(action_uid): url = f"https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/{action_uid}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: print( "GET response:", get_post_profile_enrichment_status("89cb9618-945f-40f2-a147-86fd508933c2") ) except Exception as error: print("Error:", error) ``` ```java 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) { getPostProfileEnrichmentStatus("89cb9618-945f-40f2-a147-86fd508933c2"); } private static void getPostProfileEnrichmentStatus(String actionUid) { try { String url = "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/" + actionUid; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new RuntimeException( "GET request failed: " + response.statusCode() + ", " + response.body() ); } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require("axios"); async function getPostProfileEnrichmentStatus(actionUid) { const url = `https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/${actionUid}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers }); console.log("GET response:", response.data); } catch (error) { console.error( "GET request failed:", error.response ? error.response.status : error.message ); } } getPostProfileEnrichmentStatus("89cb9618-945f-40f2-a147-86fd508933c2"); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $actionUid = '89cb9618-945f-40f2-a147-86fd508933c2'; try { $response = $client->get( 'linkedin/post_profiles_collectors/enrichment_actions/' . $actionUid ); 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 The enrichment action status and its current progress. ```json { "action_uid": "89cb9618-945f-40f2-a147-86fd508933c2", "status": "completed", "processed": 15, "total": 15, "percentage": 100, "cancel_requested": false, "submitted_at": "2026-08-20T16:06:41+02:00", "started_at": "2026-08-20T16:06:42+02:00", "finished_at": "2026-08-20T16:07:15+02:00", "error_code": null, "result": { "total_rows": 15, "failed_rows": 1, "completed_rows": 14, "processed_rows": 15, "failed_row_samples": [ { "code": "NOT_ENRICHED", "row_id": "b3a4b3a4-0199-4cf7-a3d6-8bf9f55cf905", "message": "Lead finder did not enrich this row" } ], "error_summary_by_code": { "NOT_ENRICHED": 1 } } } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `action_uid` | string | Enrichment action UUID | | `status` | string | Action status: `queued` - waiting to start`running` - enrichment is in progress`cancel_requested` - a cancellation request was recorded`completed` - processing finished`failed` - processing failed`canceled` - processing was canceled | | `processed` | integer | Number of list rows processed so far | | `total` | integer | Total number of list rows to be processed | | `percentage` | integer | Processing progress as a percentage | | `cancel_requested` | boolean | Whether cancellation was requested for the action | | `submitted_at` | string | Submission time in ISO 8601 format, including an offset | | `started_at` | string | Start time in ISO 8601 format, including an offset; omitted until processing starts | | `finished_at` | string | Finish time in ISO 8601 format, including an offset; omitted until processing finishes | | `error_code` | string/null | Action-level error code | | `result` | object | Processing summary | |   └─ `total_rows` | integer | Total number of rows included in the result | |   └─ `failed_rows` | integer | Number of rows that could not be enriched | |   └─ `completed_rows` | integer | Number of rows processed successfully | |   └─ `processed_rows` | integer | Total number of processed rows | |   └─ `failed_row_samples` | array[object] | Sample errors for rows that could not be enriched; empty when there are no failed samples | |     └─ `code` | string | Row error code, such as `NOT_ENRICHED` | |     └─ `row_id` | string | UUID of the affected list row | |     └─ `message` | string | Human-readable row error details | |   └─ `error_summary_by_code` | object | Number of failed rows grouped by error code; empty when there are no errors | `action_uid` is not a valid UUID. ```json { "type": "validation_error", "code": "invalid_fields", "message": "Invalid field(s)", "request_id": "3a1ada93-b420-4373-8c23-92f48a326b10", "fields": [ { "field": "action_uid", "issue": "must_be_valid_uuid", "value": "not-a-uuid" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `validation_error` | | `code` | string | Error code: `invalid_fields` | | `message` | string | Human-readable error summary | | `request_id` | string/null | Request identifier, or `null` when unavailable | | `fields` | array[object] | Invalid path parameters | |   └─ `field` | string | Invalid field: `action_uid` | |   └─ `issue` | string | Validation issue: `must_be_valid_uuid` | |   └─ `value` | string | Rejected path value | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "title": "Unauthorized", "status": 401, "detail": "Invalid api key", "timestamp": "2026-08-25 10:00:00" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | HTTP status code | | `detail` | string | Detailed message explaining the error | | `timestamp` | string | Timestamp when the error occurred | No enrichment action with this UUID exists for the authenticated Woodpecker account. ```json { "type": "validation_error", "code": "not_found", "message": "Action not found: 89cb9618-945f-40f2-a147-86fd508933c2", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type returned by Datasheets Manager | | `code` | string | Error code: `not_found` | | `message` | string | Human-readable error details | | `request_id` | string/null | Request identifier, or null when unavailable | Unexpected server error. Try the request again later. ```json { "type": "internal_error", "code": "internal_server_error", "message": "An unexpected error occurred on the server.", "request_id": "3a1ada93-b420-4373-8c23-92f48a326b10" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `internal_error` | | `code` | string | Error code: `internal_server_error` | | `message` | string | Human-readable error summary | | `request_id` | string/null | Request identifier, or `null` when unavailable | --- ## Collecting profiles Use these endpoints to collect profiles and engagement data from LinkedIn posts, enrich the collected profiles, and save the results in lists in Woodpecker. ## Collect profiles from LinkedIn posts Collect profiles of people who reacted to or commented on selected LinkedIn posts. Woodpecker saves the profiles, reaction types, and comments to a new [list](https://woodpecker.co/help-center/en/articles/15302423). You do not need a connected LinkedIn account to use this workflow. ### How the workflow works 1. **Start collection.** Submit LinkedIn post URLs and choose whether to collect comments, reactions, or both. 2. **Check progress.** Use the returned collector IDs to check the status of the request. Processing is asynchronous and typically takes 5–10 minutes. 3. **Use the results.** Woodpecker saves the collected profiles and engagement data to a new list. Open the list in Woodpecker to work with the results, add the profiles to campaigns, or continue by enriching them. 4. **Enrich the profiles.** Start enrichment for the list to add available contact and company data to the collected profiles. 5. **Check enrichment progress.** Use the returned action ID to monitor the enrichment. The enriched data is saved to the same list. After collecting comments and reactions, the list will contain `LinkedIn Profile URL`, `LinkedIn Post`, `Reaction`, and `Comment text` columns. Enrichment adds available contact and company data to the same list. Lists are available in the Woodpecker app but are not exposed as a separate public API resource yet. [Learn how lists work in Woodpecker](https://woodpecker.co/help-center/en/articles/15302423-lists-in-woodpecker). :::info Credit usage Collection can use account credits. Collecting profiles from reactions costs 10 credits per 100 profiles (0.1 credit per profile), while collecting profiles from comments costs 30 credits per 100 profiles (0.3 credit per profile). Credits are used only for profiles that are successfully collected. The GET endpoints used to check collection status do not use credits. Enrichment costs 1.5 credits per successfully enriched profile. ::: ### Available endpoints | Endpoint | Method and path | Use it to | |----------|-----------------|-----------| | [Collect profiles from posts](post-collect-profiles.mdx) | `POST /rest/v2/linkedin/post_profiles_collectors/enqueue` | Create a list and start one or more collection jobs | | [List collection jobs](get-collection-jobs.mdx) | `GET /rest/v2/linkedin/post_profiles_collectors` | List recent collection jobs and check their statuses | | [Get a collection job](get-collection-job.mdx) | `GET /rest/v2/linkedin/post_profiles_collectors/{uid}` | Get the details and status of a collection job | | [Enrich collected profiles](post-enrich-profiles.mdx) | `POST /rest/v2/linkedin/post_profiles_collectors/enrichment_actions/enqueue` | Enrich the profiles in a collected list | | [Get enrichment status](get-enrichment-status.mdx) | `GET /rest/v2/linkedin/post_profiles_collectors/enrichment_actions/{action_uid}` | Check the status of an enrichment action | --- ## Collect profiles from posts Collect LinkedIn profiles of people who reacted to or commented on selected posts and save the results to a new [list in Woodpecker](https://woodpecker.co/help-center/en/articles/15302423). This request creates a collector for each post and selected engagement type. Processing is asynchronous - use [list collection jobs](get-collection-jobs.mdx) or [get a collection job](get-collection-job.mdx) to check progress. Each request creates one list containing the collected **profile URLs, post URLs, reaction types, and comment text**. Open it in Woodpecker to work with the results. ## Request :::info Credit usage This request can use account credits. Collecting profiles from reactions costs 10 credits per 100 profiles (0.1 credit per profile), while collecting profiles from comments costs 30 credits per 100 profiles (0.3 credit per profile). Credits are used only for profiles that are successfully collected. GET endpoints used to check collection status do not use credits. ::: Most collection jobs complete within 5-10 minutes. Check progress using either GET endpoint. Submitting the same post and source again does not speed up collection and may be rejected while a matching collector is still active. ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enqueue ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body Submit up to 100 unique LinkedIn post URLs. In `collect_from`, set comments, reactions, or both to true. Both options save the LinkedIn profile URL and post URL to the new list. Reaction collection also saves the reaction type, while comment collection saves the comment text. ```json { "post_urls": [ "https://www.linkedin.com/posts/linkedin_post_url" ], "collect_from": { "comments": true, "reactions": true }, "target_new_list_name": "ICP profiles" } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `post_urls` | array[string] | Yes | 1 to 100 unique LinkedIn post URLs. Each URL must start with `https://www.linkedin.com/posts/` | | `collect_from` | object | Yes | Sources from which profiles should be collected | |   └─ `comments` | boolean | Yes | Set to `true` to collect profiles that commented on the posts, together with their comment text | |   └─ `reactions` | boolean | Yes | Set to `true` to collect profiles that reacted to the posts, together with their reaction type | | `target_new_list_name` | string | Yes | Base name for the new list; Woodpecker appends ` - LinkedIn Leads` at the end | ### Request samples #### Collect profiles from comments and reactions ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enqueue" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "post_urls": [ "https://www.linkedin.com/posts/linkedin_post_url" ], "collect_from": { "comments": true, "reactions": true }, "target_new_list_name": "Pied Piper post engagement" }' ``` ```Python import requests def enqueue_post_profile_collectors(): url = "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enqueue" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "post_urls": [ "https://www.linkedin.com/posts/linkedin_post_url" ], "collect_from": { "comments": True, "reactions": True }, "target_new_list_name": "Pied Piper post engagement" } response = requests.post(url, headers=headers, json=payload) if response.status_code == 202: return response.json() raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: print("POST response:", enqueue_post_profile_collectors()) except Exception as error: print("Error:", error) ``` ```java 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/linkedin/post_profiles_collectors/enqueue"; String jsonData = """ { "post_urls": [ "https://www.linkedin.com/posts/linkedin_post_url" ], "collect_from": { "comments": true, "reactions": true }, "target_new_list_name": "Pied Piper post engagement" } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", "{YOUR_API_KEY}") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("POST response: " + response.body()); } else { throw new RuntimeException( "POST request failed: " + response.statusCode() + ", " + response.body() ); } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require("axios"); async function enqueuePostProfileCollectors() { const url = "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enqueue"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { post_urls: [ "https://www.linkedin.com/posts/linkedin_post_url" ], collect_from: { comments: true, reactions: true }, target_new_list_name: "Pied Piper post engagement" }; 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 ); } } enqueuePostProfileCollectors(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('linkedin/post_profiles_collectors/enqueue', [ 'json' => [ 'post_urls' => [ 'https://www.linkedin.com/posts/linkedin_post_url', ], 'collect_from' => [ 'comments' => true, 'reactions' => true, ], 'target_new_list_name' => 'Pied Piper post engagement', ], ]); 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 The collectors were accepted for processing. Each object represents one post and one source. Store each `collector_uid` and use [get a collection job](get-collection-job.mdx) to check its status ```json { "accepted_posts": [ { "collector_uid": "e9e6abc4-799e-4c24-b412-7927b1db919d", "post_url": "https://www.linkedin.com/posts/linkedin_post_url", "target_list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1", "collect_from": "REACTIONS" }, { "collector_uid": "caf15107-53dc-4f19-8920-ddfbd95b44c3", "post_url": "https://www.linkedin.com/posts/linkedin_post_url", "target_list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1", "collect_from": "COMMENTS" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `accepted_posts` | array[object] | Collectors accepted for asynchronous processing | |   └─ `collector_uid` | string | Collector UUID to use with the get endpoint | |   └─ `post_url` | string | LinkedIn post URL submitted for this collector | |   └─ `target_list_uid` | string | UUID of the new list shared by all collectors from this request | |   └─ `collect_from` | string | Collection source: `REACTIONS` or `COMMENTS` | Returned when the request body is missing, is not valid JSON, or omits a required field ```json { "type": "validation_error", "code": "invalid_fields", "message": "Invalid field(s)", "requestId": "4a3cc945-f502-4f83-b0ee-5d7fd1f38548", "fields": [ { "field": "post_urls", "issue": "required", "value": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `validation_error` | | `code` | string | Error code: `invalid_fields` | | `message` | string | Human-readable error summary | | `requestId` | string/null | Request identifier, or `null` when unavailable | | `fields` | array[object] | Invalid or missing request fields | |   └─ `field` | string | Field path, such as `body`, `post_urls`, `collect_from.comments`, etc | |   └─ `issue` | string | Shape error: `required` or `invalid` | |   └─ `value` | any/null | Rejected value, or `null` when no value was supplied | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "Invalid api key", "timestamp": "2026-08-05 10:00: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 | Returned when the request body contains invalid field values. The following example shows an empty `post_urls` array. ```json { "type": "validation_error", "code": "invalid_fields", "message": "Invalid field(s)", "requestId": "80820418-a95c-4b37-9fd3-68a4c9d7e6a0", "fields": [ { "field": "post_urls", "issue": "empty", "value": [] } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `code` | string | Error code | | `message` | string | Error message | | `requestId` | string/null | Request identifier when available | | `fields` | array[object] | Fields that failed validation | |   └─ `field` | string | Field with a validation issue | |   └─ `issue` | string | Validation issue: `empty`, `too_many`, `invalid format, expected: ...`, `duplicated`, `already_exists`, `at_least_one_required`, or `required` | |   └─ `value` | any/null | Rejected value, or `null` when unavailable | Unexpected error, please try again later ```json { "type": "internal_error", "code": "internal_server_error", "message": "An unexpected error occurred on the server.", "requestId": "4a3cc945-f502-4f83-b0ee-5d7fd1f38548" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `internal_error` | | `code` | string | Error code: `internal_server_error` | | `message` | string | Human-readable error summary | | `requestId` | string/null | Request identifier, or `null` when unavailable | --- ## Enrich collected profiles Enrich profiles [collected from a LinkedIn post](post-collect-profiles.mdx) with available contact and company data. The enrichment adds the data to the same list, so you can continue working with the profiles in Woodpecker. This request starts an asynchronous enrichment job. Use the returned `action_uid` with [get enrichment status](get-enrichment-status.mdx) to check its progress. ## Request :::info Credit usage Each successfully enriched profile costs 1.5 credits. Credits are charged only for profiles where enrichment finds data. ::: The request processes all rows in the list. It creates or reuses these output columns: **Email, First name, Last name, Company, Website, Industry, Title, Tags, Address, City, State, and Country**. A column is populated only when enrichment finds the corresponding data. A profile is considered successfully enriched when it returns at least one value. ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/enqueue ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body Use the UUID of a list created by [collecting profiles from posts](get-collection-jobs.mdx). ```json { "list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1" } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `list_uid` | string | Yes | UUID of the list created by the LinkedIn post profile collection flow | ### Request samples #### Enrich all profiles in a collected list ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/enqueue" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1" }' ``` ```Python import requests def enqueue_post_profile_enrichment(): url = "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/enqueue" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1" } response = requests.post(url, headers=headers, json=payload) if response.status_code == 202: return response.json() raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: print("POST response:", enqueue_post_profile_enrichment()) except Exception as error: print("Error:", error) ``` ```java 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/linkedin/post_profiles_collectors/enrichment_actions/enqueue"; String jsonData = """ { "list_uid": "5f17c0d2-5100-4bb9-a312-fd17f27f94c1" } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", "{YOUR_API_KEY}") .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("POST response: " + response.body()); } else { throw new RuntimeException( "POST request failed: " + response.statusCode() + ", " + response.body() ); } } catch (Exception e) { System.err.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require("axios"); async function enqueuePostProfileEnrichment() { const url = "https://api.woodpecker.co/rest/v2/linkedin/post_profiles_collectors/enrichment_actions/enqueue"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { list_uid: "5f17c0d2-5100-4bb9-a312-fd17f27f94c1" }; 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 ); } } enqueuePostProfileEnrichment(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('linkedin/post_profiles_collectors/enrichment_actions/enqueue', [ 'json' => [ 'list_uid' => '5f17c0d2-5100-4bb9-a312-fd17f27f94c1', ], ]); 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 The enrichment action was accepted for processing. Save the returned `action_uid`. You will need it to [check the enrichment status](get-enrichment-status.mdx). ```json { "action_uid": "89cb9618-945f-40f2-a147-86fd508933c2" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `action_uid` | string | Enrichment action UUID to use when checking its status | Returned when the request body is missing, is not valid JSON, does not include `list_uid`, or the list doesn't contain any entries. The following example shows a missing `list_uid`. ```json { "type": "validation_error", "code": "invalid_fields", "message": "Invalid field(s)", "request_id": "3a1ada93-b420-4373-8c23-92f48a326b10", "fields": [ { "field": "list_uid", "issue": "required", "value": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `validation_error` | | `code` | string | Error code: `invalid_fields`/`bad_request` | | `message` | string | Human-readable error summary | | `request_id` | string/null | Request identifier, or `null` when unavailable | | `fields` | array[object]/null | Invalid or missing request fields | |   └─ `field` | string/null | Field with a validation issue, such as `body` or `list_uid` | |   └─ `issue` | string/null | Validation issue: `required` or `invalid` | |   └─ `value` | any/null | Rejected value, or `null` when no value was supplied | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx). ```json { "title": "Unauthorized", "status": 401, "detail": "Invalid api key", "timestamp": "2026-08-25 10:00:00" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | HTTP status code | | `detail` | string | Detailed message explaining the error | | `timestamp` | string | Timestamp when the error occurred | Some of the required input columns, like `LinkedIn Profile URL`, were deleted or modified. ```json { "type": "validation_error", "code": "not_found", "message": "Some input columns were not found", "request_id": "3a1ada93-b420-4373-8c23-92f48a326b10" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `validation_error` | | `code` | string | Error code: `not_found` | | `message` | string | Human-readable error summary | | `request_id` | string/null | Request identifier, or `null` when unavailable | Returned when the list was not created by the LinkedIn post profile collection flow, or the enrichment cannot be enqueued. If your account does not have enough credits, [review your billing](https://app.woodpecker.co/panel#settings/billing/subscription-plan), add credits, and submit a new request. ```json { "type": "validation_error", "code": "target_datasheet_collector_not_found", "message": "No LinkedIn post profiles collector was found for the target list.", "request_id": "3a1ada93-b420-4373-8c23-92f48a326b10" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `validation_error` | | `code` | string | Error code: `target_datasheet_collector_not_found`, `target_datasheet_profile_url_column_not_found`, `target_datasheet_enrichment_failed`, or `insufficient_credits` | | `message` | string | Human-readable error summary | | `request_id` | string/null | Request identifier, or `null` when unavailable | Unexpected server error. Try the request again later. ```json { "type": "internal_error", "code": "internal_server_error", "message": "An unexpected error occurred on the server.", "request_id": "3a1ada93-b420-4373-8c23-92f48a326b10" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type: `internal_error` | | `code` | string | Error code: `internal_server_error` | | `message` | string | Human-readable error summary | | `request_id` | string/null | Request identifier, or `null` when unavailable | --- ## Get a list of LinkedIn accounts Retrieve a list of LinkedIn accounts associated with your Woodpecker account, including each account's status, user information, and subscription level. To react to connection changes without polling this endpoint, subscribe to the [linkedin_automation_account_connected](/docs/webhooks/linkedin-account-connected.mdx) and [linkedin_automation_account_disconnected](/docs/webhooks/linkedin-account-disconnected.mdx) webhooks. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/linkedin_accounts ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Description | | --------- | -------- | ------------------------------------------------------------------------------------------- | | `status` | No | Filter accounts by their `session_status`. Available statuses (case-sensitive): `CONNECTED`, `DISCONNECTED`, `WAITING_FOR_CONNECTION` | ### Request samples #### Some sample request ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/linkedin_accounts" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getLinkedinAccounts(): url = "https://api.woodpecker.co/rest/v2/linkedin_accounts" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getLinkedinAccounts() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getLinkedinAccounts(); } public static void getLinkedinAccounts() { try { String url = "https://api.woodpecker.co/rest/v2/linkedin_accounts"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getLinkedinAccounts() { const url = "https://api.woodpecker.co/rest/v2/linkedin_accounts"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getLinkedinAccounts(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('linkedin_accounts'); 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 Request processed successfully. The below example showcases 2 different LinkedIn accounts. If you don't have any accounts connected, or they don't match the requested filters, `linkedin_accounts` will be an empty array. ```json { "linkedin_accounts": [ { "id": 111111, "username": null, "session_status": "CONNECTED", "full_name": "Jim Halpert", "linkedin_url": "https://www.linkedin.com/in/jim-halpert-123abc", "level": "SALES_NAVIGATOR" }, { "id": 111112, "username": null, "session_status": "DISCONNECTED", "full_name": "Jimothy Halpert", "linkedin_url": "https://www.linkedin.com/in/jimothy-halpert-321xyz", "level": "CLASSIC" } ] } ``` #### Body schema | Field | Type | Description | |--------|------|-------------| | `linkedin_accounts` | array[object] | An array of LinkedIn accounts associated with the Woodpecker account | | └─`[].id` | integer | Unique identifier of a LinkedIn account in Woodpecker | | └─`[].username` | null / string | Deprecated. Username previously used to connect the account. This field is no longer populated for new accounts, may be `null`, and may be removed entirely in the future | | └─`[].session_status` | string | Current connection status of the account. Available values: `CONNECTED`, `DISCONNECTED`, `WAITING_FOR_CONNECTION` - The account has been added to Woodpecker but hasn't been successfully connected. Requires user action to complete the process | | └─`[].full_name` | string | Full name associated with the LinkedIn account | | └─`[].linkedin_url` | string | LinkedIn profile URL of the account | | └─`[].level` | string | Subscription level of the account. Relates to sending limits. Available values: `CLASSIC`, `PREMIUM`, `RECRUITER_LITE`, `SALES_NAVIGATOR`, `UNKNOWN` | Please review the [request parameters](#parameters) ```json { "title": "Bad Request", "status": 400, "detail": "Value of status is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Mailbox connection batch status Review the status of the submitted connection batch using the batch ID returned by [rest/v2/mailboxes/manual_connection/bulk](post-mailboxes.mdx). This endpoint will provide an overview of the connection process and details about any potential issues. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk/{batch_id}/summary ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Retrieve batch summary ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk/{batch_id}/summary" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getManualConnectionBatchSummary(batch_id): url = f"https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk/{batch_id}/summary" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getManualConnectionBatchSummary(12345) # Example integer batch ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int batchId = 12345; // Example batch ID getManualConnectionBatchSummary(batchId); } public static void getManualConnectionBatchSummary(int batchId) { try { String url = "https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk/" + batchId + "/summary"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getManualConnectionBatchSummary(batchId) { const url = `https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk/${batchId}/summary`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getManualConnectionBatchSummary(12345); // Example integer batch ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $batchId = '{batch_id}'; try { $response = $client->get("mailboxes/manual_connection/bulk/{$batchId}/summary"); 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 An example response received when the processing has been finished and all mailboxes have been successfully connected. ```json { "processing_finished": true, "batch_size": 25, "pending_mailboxes_count": 0, "connected_mailboxes_count": 25, "conditionally_connected_mailboxes_count": 0, "failed_mailboxes_count": 0, "conditionally_connected_mailboxes": [], "failed_mailboxes": [] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `processing_finished` | boolean | Indicates if the batch processing is complete | | `batch_size` | integer | Total number of mailboxes submitted in the batch | | `pending_mailboxes_count` | integer | Number of mailboxes still being processed | | `connected_mailboxes_count` | integer | Number of successfully connected mailboxes | | `conditionally_connected_mailboxes_count` | integer | Number of mailboxes [connected conditionally](https://woodpecker.co/help-center/en/articles/5268011) (not included in `connected_mailboxes_count`) | | `failed_mailboxes_count` | integer | Number of mailboxes that failed to connect | | `conditionally_connected_mailboxes` | array | List of mailboxes connected conditionally | | `failed_mailboxes` | array | List of mailboxes that failed to connect | This example includes conditionally connected mailboxes and failed connections while some mailboxes are still being processed. Check `processing_finished` to determine whether the batch is complete. Failed connections include separate `smtp_error` and `imap_error` objects with diagnostic details when available. Either object can be `null` when no error details were recorded for that protocol. ```json { "processing_finished": false, "batch_size": 100, "pending_mailboxes_count": 20, "connected_mailboxes_count": 75, "conditionally_connected_mailboxes_count": 1, "failed_mailboxes_count": 4, "conditionally_connected_mailboxes": [ { "connection_request_id": 12345, "smtp_email": "john@conditional.co", "smtp_login": "john@conditional.co", "imap_email": "peter@conditional.co" } ], "failed_mailboxes": [ { "connection_request_id": 12346, "smtp_email": "jared.dunn@piedpiper.com", "smtp_login": "jared.dunn@piedpiper.com", "imap_email": "jared.dunn@piedpiper.com", "error_message": "SMTP account already connected", "smtp_error": null, "imap_error": null }, { "connection_request_id": 12347, "smtp_email": "jared@piedpiper.com", "smtp_login": "jared@piedpiper.com", "imap_email": "jared@piedpiper.com", "error_message": "SMTP authentication failed", "smtp_error": { "status_code": "CREDENTIAL", "short_message": "SMTP authentication failed", "long_message": "Check the SMTP login and password, then try again.", "raw_message": "535 Authentication failed" }, "imap_error": null }, { "connection_request_id": 12348, "smtp_email": "jared@getpiedpiper.com", "smtp_login": "jared@getpiedpiper.com", "imap_email": "jared@getpiedpiper.com", "error_message": "IMAP authentication failed", "smtp_error": null, "imap_error": { "status_code": "CREDENTIAL", "short_message": "Login or password may be wrong.", "long_message": "Login or password may be wrong (authentication failed)", "raw_message": "[AUTHENTICATIONFAILED] Authentication failed." } }, { "connection_request_id": 12349, "smtp_email": "jdunn@piedpiper.com", "smtp_login": "jdunn@piedpiper.com", "imap_email": "jdunn@piedpiper.com", "error_message": "Unknown error", "smtp_error": null, "imap_error": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `processing_finished` | boolean | Indicates if the batch processing is complete | | `batch_size` | integer | Total number of mailboxes submitted in the batch | | `pending_mailboxes_count` | integer | Number of mailboxes still being processed | | `connected_mailboxes_count` | integer | Number of successfully connected mailboxes | | `conditionally_connected_mailboxes_count` | integer | Number of mailboxes [connected conditionally](https://woodpecker.co/help-center/en/articles/5268011) (not included in `connected_mailboxes_count`) | | `failed_mailboxes_count` | integer | Number of mailboxes that failed to connect | | `conditionally_connected_mailboxes` | array | List of conditionally connected mailboxes | |   └─ `connection_request_id` | integer | Unique identifier of the connection request | |   └─ `smtp_email` | string | SMTP email address of the conditionally connected mailbox | |   └─ `smtp_login` | string | SMTP login of the conditionally connected mailbox | |   └─ `imap_email` | string | IMAP email address of the conditionally connected mailbox | | `failed_mailboxes` | array | List of mailboxes that failed to connect | |   └─ `connection_request_id` | integer | Unique identifier of the connection request | |   └─ `smtp_email` | string | SMTP email address of the mailbox that couldn't connect | |   └─ `smtp_login` | string | SMTP login of the mailbox that couldn't connect | |   └─ `imap_email` | string | IMAP email address of the mailbox that couldn't connect | |   └─ `error_message` | string | Connection error message. If possible, the response will indicate whether the issue is related to SMTP or IMAP connection | |   └─ `smtp_error` | object/null | SMTP error details, or `null` when there were no SMTP related issues | |     └─ `status_code` | string | Connection error code, such as `HOST` or `CREDENTIAL` | |     └─ `short_message` | string/null | Short description of the SMTP error | |     └─ `long_message` | string/null | Detailed explanation of the SMTP error | |     └─ `raw_message` | string/null | Original diagnostic message from the SMTP connection attempt | |   └─ `imap_error` | object/null | IMAP error details, or `null` when there were no IMAP related issues | |     └─ `status_code` | string | Connection error code, such as `HOST` or `CREDENTIAL` | |     └─ `short_message` | string/null | Short description of the IMAP error | |     └─ `long_message` | string/null | Detailed explanation of the IMAP error | |     └─ `raw_message` | string/null | Original diagnostic message from the IMAP connection attempt | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 | Batch not found. Please review the batch ID. ```json { "code": "BATCH_NOT_FOUND", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code `BATCH_NOT_FOUND` | | `details` | string/null | Additional information | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Get a mailbox Retrieve a configuration of a specific email account connected to your account. Each object contains details of a specific SMTP or IMAP and information about the connection settings, errors, sending limits, warm-up status and freeze dates, etc. You can also retrieve a list of all mailboxes under your account using the [/mailboxes](get-mailboxes.mdx) endpoint. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/mailboxes/{id} ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Retrieve a mailbox ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/mailboxes/{id}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getMailboxById(mailbox_id): url = f"https://api.woodpecker.co/rest/v2/mailboxes/{mailbox_id}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getMailboxById(9876) # Example mailbox ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int mailboxId = 9876; // Example mailbox ID getMailboxById(mailboxId); } public static void getMailboxById(int mailboxId) { try { String url = "https://api.woodpecker.co/rest/v2/mailboxes/" + mailboxId; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getMailboxById(mailboxId) { const url = `https://api.woodpecker.co/rest/v2/mailboxes/${mailboxId}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getMailboxById(9876); // Example mailbox ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $mailboxId = '{id}'; try { $response = $client->get("mailboxes/{$mailboxId}"); 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 Request processed successfully. Each response provides details for either SMTP or IMAP. Although both types share the same fields, the SMTP response includes additional information. The example below illustrates a response for SMTP along with its corresponding IMAP, and it includes a breakdown of the body schemas for both types. ```json { "id": 456789, "type": "SMTP", "details": { "email": "jared@getpiedpiper.com", "provider": "CUSTOM", "login": "jared@getpiedpiper.com", "server": "mail.privateemail.com", "port": 587, "from_name": "Jared from Pied Piper", "error": null, "daily_limit": 100, "sent_today": 0, "frequency_from": 300000, "frequency_to": 600000, "bcc_crm": "mycrm@pipedrivemail.com", "signature": "
Jered Dunn
Pied Pieper
", "open_url": "mail.getpiedpiper.com", "click_url": "mail.getpiedpiper.com", "unsubscribe_url": "mail.getpiedpiper.com", "freeze_account": [], "in_slot": true, "warmup_data": { "status": "RUNNING" }, "imap_id": 456790, "reconnect_required": false } } ``` #### Body schema | Field | Type | Description | |--------|------|-------------| | `id` | integer | Unique identifier of an SMTP configuration | | `type` | string | Configuration type: `SMTP` or `IMAP` | | `details` | object | Object containing all mailbox details | | └─`details.email` | string | Email address | | └─`details.provider` | string | Email provider of the mailbox | | └─`details.login` | string | Email login | | └─`details.server` | string | SMTP server | | └─`details.port` | integer/null | SMTP port. Optional field, can be null | | └─`details.from_name` | string | Display name for SMTP sender | | └─`details.error` | string/null | Mailbox error message. For SMTP, this can describe an SMTP problem or an IMAP problem affecting this SMTP. Returns `null` when the mailbox is connected without issues | | └─`details.daily_limit` | integer | Maximum daily email sending limit | | └─`details.sent_today` | integer | Emails sent in the last 24 hours | | └─`details.frequency_from` | integer | Minimum delay between sending messages (ms) | | └─`details.frequency_to` | integer | Maximum delay between sending messages (ms) | | └─`details.bcc_crm` | string | BCC email for CRM integration | | └─`details.signature` | string | Email signature in HTML format | | └─`details.open_url` | string | Tracking domain for email opens | | └─`details.click_url` | string | Tracking domain for link clicks | | └─`details.unsubscribe_url` | string |Tracking domain for unsubscribe links | | └─`details.freeze_account` | array | Array of JSON objects containing scheduled pauses in sending -`date_from` and `date_to`, ISO 8601 format | | └─`details.in_slot` | boolean | Deprecated | | └─`details.warmup_data` | object | Email warm-up configuration | | └─`details.warmup_data.status` | string | Warm-up status: `RUNNING`, `PAUSED`, `DISABLED`, `BLOCKED` | | └─`details.imap_id` | integer | Reference to linked IMAP configuration. One IMAP can be assigned to multiple SMTPs | | └─`details.reconnect_required` | boolean | Whether the returned error requires reconnecting the mailbox. Returns `false` when reconnecting is not required or there is no mailbox error | ```json { "id": 456790, "type": "IMAP", "details": { "email": "jared@getpiedpiper.com", "provider": "CUSTOM", "login": "jared@getpiedpiper.com", "server": "mail.privateemail.com", "port": 993, "error": null, "private": false } } ``` #### Body schema | Field | Type | Description | |--------|------|-------------| | `id` | integer | Unique identifier of an IMAP configuration | | `type` | string | Configuration type: `SMTP` or `IMAP` | | └─`details` | object | Object containing all mailbox details | | └─`details.email` | string | Email address | | └─`details.provider` | string | Email provider of the mailbox | | └─`details.login` | string | Email login | | └─`details.server` | string | IMAP server | | └─`details.port` | integer/null | IMAP port. Optional field, can be null | | └─`details.error` | string/null | IMAP error message. Returns `null` when the IMAP is connected without issues | | └─`details.private` | boolean | Privacy setting indicating whether Woodpecker downloads all emails or emails related to campaigns | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Get a list of mailboxes Retrieve a list and configuration of email accounts connected to your account. Each object contains details of SMTP or IMAP and information about the connection settings, errors, sending limits, warm-up status and freeze dates, and more. You can also retrieve information about one specific IMAP or SMTP using their IDs and the [/mailboxes/id](get-mailbox.mdx) endpoint. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/mailboxes ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Some sample request ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/mailboxes" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getAllMailboxes(): url = "https://api.woodpecker.co/rest/v2/mailboxes" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getAllMailboxes() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getAllMailboxes(); } public static void getAllMailboxes() { try { String url = "https://api.woodpecker.co/rest/v2/mailboxes"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getAllMailboxes() { const url = "https://api.woodpecker.co/rest/v2/mailboxes"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getAllMailboxes(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('mailboxes'); 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 Request processed successfully. The below example showcases 3 separate mailboxes using 3 different email service providers. If your account doesn't have any mailboxes connected, the response will be an empty array. ```json [ { "id": 123456, "type": "SMTP", "details": { "email": "jared.dunn@piedpiper.com", "provider": "GOOGLE", "login": "jared.dunn@piedpiper.com", "server": "smtp.gmail.com", "port": null, "from_name": "Jared Dunn", "error": null, "daily_limit": 50, "sent_today": 20, "frequency_from": 150000, "frequency_to": 600000, "bcc_crm": "", "signature": "
Email signature wrapped in HTML.
", "open_url": "sub.piedpiper.com", "click_url": "sub.piedpiper.com", "unsubscribe_url": "sub.piedpiper.com", "freeze_account": [], "in_slot": true, "warmup_data": { "status": "PAUSED" }, "imap_id": 123457, "reconnect_required": false } }, { "id": 123457, "type": "IMAP", "details": { "email": "jared.dunn@piedpiper.com", "provider": "GOOGLE", "login": "jared.dunn@piedpiper.com", "server": "imap.gmail.com", "port": null, "error": null, "private": true } }, { "id": 456789, "type": "SMTP", "details": { "email": "jared@getpiedpiper.com", "provider": "CUSTOM", "login": "jared@getpiedpiper.com", "server": "mail.privateemail.com", "port": 587, "from_name": "Jared from Pied Piper", "error": null, "daily_limit": 100, "sent_today": 0, "frequency_from": 300000, "frequency_to": 600000, "bcc_crm": "mycrm@pipedrivemail.com", "signature": "
Jered Dunn
Pied Pieper
", "open_url": "mail.getpiedpiper.com", "click_url": "mail.getpiedpiper.com", "unsubscribe_url": "mail.getpiedpiper.com", "freeze_account": [], "in_slot": true, "warmup_data": { "status": "RUNNING" }, "imap_id": 456790, "reconnect_required": false } }, { "id": 456790, "type": "IMAP", "details": { "email": "jared@getpiedpiper.com", "provider": "CUSTOM", "login": "jared@getpiedpiper.com", "server": "mail.privateemail.com", "port": 993, "error": null, "private": false } }, { "id": 987654, "type": "SMTP", "details": { "email": "erlich@usebachmanity.com", "provider": "OFFICE", "login": "erlich@usebachmanity.com", "server": "api", "port": null, "from_name": "Erlich Bachman", "error": null, "daily_limit": 30, "sent_today": 15, "frequency_from": 50000, "frequency_to": 800000, "bcc_crm": "", "signature": "
Erlich Bachman
Bachmanity
", "open_url": "email.usebachmanity.com", "click_url": "email.usebachmanity.com", "unsubscribe_url": "email.usebachmanity.com", "freeze_account": [ { "date_from": "2025-05-01T00:00:00+0100", "date_to": "2025-05-01T23:59:59+0100" } ], "in_slot": true, "warmup_data": { "status": "DISABLED" }, "imap_id": 987655, "reconnect_required": false } }, { "id": 987655, "type": "IMAP", "details": { "email": "erlich@usebachmanity.com", "provider": "OFFICE", "login": "erlich@usebachmanity.com", "server": null, "port": null, "error": null, "private": false } } ] ``` #### Body schema Each object includes details for either SMTP or IMAP. While both types share the same fields, the SMTP includes additional information. Below is a breakdown of the body schemas for both types. | Field | Type | Description | |--------|------|-------------| | `id` | integer | Unique identifier of an SMTP configuration | | `type` | string | Configuration type: `SMTP` or `IMAP` | | `details` | object | Object containing all mailbox details | | └─`details.email` | string | Email address | | └─`details.provider` | string | Email provider of the mailbox | | └─`details.login` | string | Email login | | └─`details.server` | string | SMTP server | | └─`details.port` | integer/null | SMTP port. Optional field, can be null | | └─`details.from_name` | string | Display name for SMTP sender | | └─`details.error` | string/null | Mailbox error message. For SMTP, this can describe an SMTP problem or an IMAP problem affecting this SMTP. Returns `null` when the mailbox is connected without issues | | └─`details.daily_limit` | integer | Maximum daily email sending limit | | └─`details.sent_today` | integer | Emails sent in the last 24 hours | | └─`details.frequency_from` | integer | Minimum delay between sending messages (ms) | | └─`details.frequency_to` | integer | Maximum delay between sending messages (ms) | | └─`details.bcc_crm` | string | BCC email for CRM integration | | └─`details.signature` | string | signature in HTML format | | └─`details.open_url` | string | Tracking domain for email opens | | └─`details.click_url` | string | Tracking domain for link clicks | | └─`details.unsubscribe_url` | string |Tracking domain for unsubscribe links | | └─`details.freeze_account` | array | Array of JSON objects containing scheduled pauses in sending -`date_from` and `date_to`, ISO 8601 format | | └─`details.in_slot` | boolean | Deprecated | | └─`details.warmup_data` | object | Email warm-up configuration | | └─`details.warmup_data.status` | string | Warm-up status: `RUNNING`, `PAUSED`, `DISABLED`, `BLOCKED` | | └─`details.imap_id` | integer | Reference to linked IMAP configuration. One IMAP can be assigned to multiple SMTPs | | └─`details.reconnect_required` | boolean | Whether the returned error requires reconnecting the mailbox. Returns `false` when reconnecting is not required or there is no mailbox error | | Field | Type | Description | |--------|------|-------------| | `id` | integer | Unique identifier of an IMAP configuration | | `type` | string | Configuration type: `SMTP` or `IMAP` | | └─`details` | object | Object containing all mailbox details | | └─`details.email` | string | Email address | | └─`details.provider` | string | Email provider of the mailbox | | └─`details.login` | string | Email login | | └─`details.server` | string | IMAP server | | └─`details.port` | integer/null | IMAP port. Optional field, can be null | | └─`details.error` | string/null | IMAP error message. Returns `null` when the IMAP is connected without issues | | └─`details.private` | boolean | Privacy setting indicating whether Woodpecker downloads all emails or emails related to campaigns | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Mailboxes The `/mailboxes` API enables you to manage mailboxes associated with your account. Use these endpoints to list, retrieve, connect, and update mailboxes, or to review mailbox connection batch status. ## Available endpoints The table below lists every documented endpoint under `/rest/v2/mailboxes`. | Endpoint | Method and path | Use it to | |----------|-----------------|-----------| | [Get a list of mailboxes](get-mailboxes.mdx) | `GET /rest/v2/mailboxes` | Retrieve mailboxes connected to your account | | [Get a mailbox](get-mailbox.mdx) | `GET /rest/v2/mailboxes/{id}` | Retrieve one SMTP or IMAP mailbox configuration | | [Add mailboxes in bulk](post-mailboxes.mdx) | `POST /rest/v2/mailboxes/manual_connection/bulk` | Connect one or more mailboxes with SMTP and IMAP credentials | | [Mailbox connection batch status](get-batch-summary.mdx) | `GET /rest/v2/mailboxes/manual_connection/bulk/{batch_id}/summary` | Review the status of a submitted mailbox connection batch | | [Update mailbox](update-mailbox.mdx) | `PATCH /rest/v2/mailboxes/{smtp_mailbox_id}` | Update an SMTP mailbox footer | | [List Microsoft Graph credentials](microsoft/get-credentials.mdx) | `GET /rest/v2/mailboxes/microsoft/credentials` | Retrieve saved Microsoft Graph app-only credentials | | [Create a Microsoft Graph credential](microsoft/post-credentials.mdx) | `POST /rest/v2/mailboxes/microsoft/credentials` | Save tenant and app credentials for Microsoft Graph app-only access | | [Update a Microsoft Graph credential](microsoft/patch-credentials.mdx) | `PATCH /rest/v2/mailboxes/microsoft/credentials/{credential_id}` | Rename a credential or rotate its client secret | | [Delete a Microsoft Graph credential](microsoft/delete-credentials.mdx) | `DELETE /rest/v2/mailboxes/microsoft/credentials/{credential_id}` | Remove a credential that is not used by an active mailbox | | [Add Microsoft mailboxes in bulk](microsoft/post-ms-mailboxes.mdx) | `POST /rest/v2/mailboxes/microsoft/bulk` | Connect one or more Microsoft mailboxes with app-only authentication | --- ## Delete a Microsoft Graph credential Delete a Microsoft Graph app-only credential. Woodpecker blocks deletion when the credential is used by an active Microsoft mailbox. ## Request ### Endpoint ```text DELETE https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/{credential_id} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Delete a credential ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/{credential_id}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def delete_microsoft_graph_credential(credential_id): url = f"https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/{credential_id}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.delete(url, headers=headers) if response.status_code == 204: print("Credential deleted.") else: print("Request failed:", response.status_code, response.text) if __name__ == "__main__": delete_microsoft_graph_credential(22334455) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { deleteMicrosoftGraphCredential(22334455); } public static void deleteMicrosoftGraphCredential(int credentialId) { try { String url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/" + credentialId; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", API_KEY) .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 204) { System.out.println("Credential deleted."); } else { System.err.println("Request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function deleteMicrosoftGraphCredential(credentialId) { const url = `https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/${credentialId}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.delete(url, { headers }); if (response.status === 204) { console.log("Credential deleted."); } else { console.error("Request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } deleteMicrosoftGraphCredential(22334455); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->delete("mailboxes/microsoft/credentials/{$credentialId}"); echo $response->getStatusCode(), "\n"; } catch (RequestException $e) { echo "Error: ", $e->getMessage(), "\n"; if ($e->hasResponse()) { echo $e->getResponse()->getBody(), "\n"; } } ``` ## Response ### Response examples The credential was deleted. ```text Status: 204 Body: none ``` An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx). ```json { "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. ```json { "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 | Invalid request or malformed request syntax. The request can also fail when the credential cannot be deleted. ```json { "message": "MICROSOFT_GRAPH_CREDENTIAL_IN_USE" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `message` | string | Validation error message. Possible values: `Microsoft Graph credential not found` - no credential exists for the provided `credential_id``MICROSOFT_GRAPH_CREDENTIAL_IN_USE` - active Microsoft mailboxes use this credential; delete those mailboxes before deleting the credential | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## List Microsoft Graph credentials Retrieve Microsoft Graph app-only credentials saved for your account. Use this endpoint to find the `credential_id` required when connecting Microsoft mailboxes in bulk. The response does not include client secrets. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Retrieve credentials ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def list_microsoft_graph_credentials(): url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print(response.json()) else: print("Request failed:", response.status_code, response.text) if __name__ == "__main__": list_microsoft_graph_credentials() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { listMicrosoftGraphCredentials(); } public static void listMicrosoftGraphCredentials() { try { String url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println(response.body()); } else { System.err.println("Request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function listMicrosoftGraphCredentials() { const url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers }); if (response.status === 200) { console.log(response.data); } else { console.error("Request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } listMicrosoftGraphCredentials(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('mailboxes/microsoft/credentials'); 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 Request processed successfully. If there are no credentials saved in your account, an empty `credentials` array will be returned. ```json { "credentials": [ { "credential_id": 22334455, "tenant_id": "bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32", "client_id": "e1c6ac69-480e-4a17-9b85-3467967bd179", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `credentials` | array | Saved Microsoft Graph credentials | |   └─`credential_id` | integer | Credential ID to use when connecting Microsoft mailboxes | |   └─`tenant_id` | string | Microsoft Entra tenant ID | |   └─`client_id` | string | Microsoft Entra application client ID | |   └─`secret_expires_at` | string | Client secret expiration date in ISO 8601 format | |   └─`name` | string | Credential name | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx). ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Microsoft mailboxes Use the Microsoft mailbox endpoints to connect Microsoft 365 mailboxes with Microsoft Graph app-only authentication. This workflow uses a Microsoft Entra ID app registration instead of mailbox passwords or user-based OAuth consent. This method is designed for admin-managed or bulk mailbox connections. If a user only needs to connect their own mailbox, they can use the standard Microsoft OAuth connection flow in the Woodpecker app. First, create a Microsoft Graph credential with the tenant ID, client ID, and client secret from your Microsoft app registration. At minimum, the app registration requires the following Microsoft Graph permissions: `Mail.Read`, `Mail.ReadWrite`, `Mail.Send`, and `User.Read`. Then, use the credential to connect one or more Microsoft mailboxes in bulk. For Microsoft's setup steps, see [Register an application with the Microsoft identity platform](https://learn.microsoft.com/en-us/graph/auth-register-app-v2). ## Available endpoints | Endpoint | Method and path | Use it to | |----------|-----------------|-----------| | [List Microsoft Graph credentials](get-credentials.mdx) | `GET /rest/v2/mailboxes/microsoft/credentials` | Retrieve saved Microsoft Graph app-only credentials. | | [Create a Microsoft Graph credential](post-credentials.mdx) | `POST /rest/v2/mailboxes/microsoft/credentials` | Save tenant and app credentials for Microsoft Graph app-only access. | | [Update a Microsoft Graph credential](patch-credentials.mdx) | `PATCH /rest/v2/mailboxes/microsoft/credentials/{credential_id}` | Rename a credential or rotate its client secret. | | [Delete a Microsoft Graph credential](delete-credentials.mdx) | `DELETE /rest/v2/mailboxes/microsoft/credentials/{credential_id}` | Remove a credential that is not used by an active mailbox. | | [Add Microsoft mailboxes in bulk](post-ms-mailboxes.mdx) | `POST /rest/v2/mailboxes/microsoft/bulk` | Connect one or more Microsoft mailboxes with app-only authentication. | --- ## Update a Microsoft Graph credential Update a Microsoft Graph app-only credential. You can rename the credential, or rotate its client secret by sending both `client_secret` and `secret_expires_at` in the same request. ## Request ### Endpoint ```text PATCH https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/{credential_id} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "client_secret": "new-your-client-secret", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app - production" } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `client_secret` | string | No* | New client secret for the Microsoft app registration | | `secret_expires_at` | string | No* | Client secret expiration date in ISO 8601 format | | `name` | string | No | Credential name | \*If you send either `client_secret` or `secret_expires_at`, both fields are required. ### Request samples #### Rotate a credential secret ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/{credential_id}" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "client_secret": "new-your-client-secret", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app - production" }' ``` ```python import requests def update_microsoft_graph_credential(credential_id): url = f"https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/{credential_id}" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json", } payload = { "client_secret": "new-your-client-secret", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app - production", } response = requests.patch(url, headers=headers, json=payload) if response.status_code == 200: print(response.json()) else: print("Request failed:", response.status_code, response.text) if __name__ == "__main__": update_microsoft_graph_credential(22334455) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { updateMicrosoftGraphCredential(22334455); } public static void updateMicrosoftGraphCredential(int credentialId) { try { String url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/" + credentialId; String jsonData = """ { "client_secret": "new-your-client-secret", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app - production" } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println(response.body()); } else { System.err.println("Request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function updateMicrosoftGraphCredential(credentialId) { const url = `https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials/${credentialId}`; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { client_secret: "new-your-client-secret", secret_expires_at: "2027-11-28T23:59:59Z", name: "MS Graph app - production" }; try { const response = await axios.patch(url, data, { headers }); if (response.status === 200) { console.log(response.data); } else { console.error("Request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } updateMicrosoftGraphCredential(22334455); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->patch("mailboxes/microsoft/credentials/{$credentialId}", [ 'json' => [ 'client_secret' => 'new-your-client-secret', 'secret_expires_at' => '2027-11-28T23:59:59Z', 'name' => 'MS Graph app - production', ], ]); 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 The credential was updated. ```json { "credential_id": 22334455, "tenant_id": "bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32", "client_id": "e1c6ac69-480e-4a17-9b85-3467967bd179", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app - production" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `credential_id` | integer | Updated credential ID | | `tenant_id` | string | Microsoft Entra tenant ID | | `client_id` | string | Microsoft Entra application client ID | | `secret_expires_at` | string | Client secret expiration date in ISO 8601 format | | `name` | string | Credential name | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx). ```json { "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. ```json { "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 | Invalid request or malformed request syntax. The request can also fail when the body is valid JSON, but the credential cannot be updated. ```json { "message": "CLIENT_SECRET_AND_EXPIRY_MUST_BE_PROVIDED_TOGETHER" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `message` | string | Validation error message. Possible values: `Microsoft Graph credential not found` - no credential exists for the provided `credential_id``CLIENT_SECRET_AND_EXPIRY_MUST_BE_PROVIDED_TOGETHER` - send both `client_secret` and `secret_expires_at` when rotating a secret`MISSING_SECRET_EXPIRES_AT` - `secret_expires_at` is missing`SECRET_EXPIRES_AT_NOT_IN_FUTURE` - `secret_expires_at` is not in the future`INVALID_CLIENT_SECRET` - Microsoft Graph rejected the provided credential | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Create a Microsoft Graph credential Create a Microsoft Graph app-only credential for connecting Microsoft mailboxes. Before saving the credential, Woodpecker verifies that the provided tenant ID, client ID, and client secret can be used to retrieve a Microsoft Graph access token. The `client_secret` is encrypted before it is stored and is not returned in the response. ## Request ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "tenant_id": "bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32", "client_id": "e1c6ac69-480e-4a17-9b85-3467967bd179", "client_secret": "your-client-secret", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app" } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `tenant_id` | string | Yes | Microsoft Entra tenant ID | | `client_id` | string | Yes | Microsoft Entra application client ID | | `client_secret` | string | Yes | Client secret for the Microsoft app registration | | `secret_expires_at` | string | Yes | Client secret expiration date in ISO 8601 format | | `name` | string | No | Credential name. If omitted, Woodpecker assigns a default name | ### Request samples #### Create a credential ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "tenant_id": "bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32", "client_id": "e1c6ac69-480e-4a17-9b85-3467967bd179", "client_secret": "your-client-secret", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app" }' ``` ```python import requests def create_microsoft_graph_credential(): url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json", } payload = { "tenant_id": "bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32", "client_id": "e1c6ac69-480e-4a17-9b85-3467967bd179", "client_secret": "your-client-secret", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app", } response = requests.post(url, headers=headers, json=payload) if response.status_code == 201: print(response.json()) else: print("Request failed:", response.status_code, response.text) if __name__ == "__main__": create_microsoft_graph_credential() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { createMicrosoftGraphCredential(); } public static void createMicrosoftGraphCredential() { try { String url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials"; String jsonData = """ { "tenant_id": "bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32", "client_id": "e1c6ac69-480e-4a17-9b85-3467967bd179", "client_secret": "your-client-secret", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app" } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 201) { System.out.println(response.body()); } else { System.err.println("Request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function createMicrosoftGraphCredential() { const url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/credentials"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { tenant_id: "bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32", client_id: "e1c6ac69-480e-4a17-9b85-3467967bd179", client_secret: "your-client-secret", secret_expires_at: "2027-11-28T23:59:59Z", name: "MS Graph app" }; try { const response = await axios.post(url, data, { headers }); if (response.status === 201) { console.log(response.data); } else { console.error("Request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } createMicrosoftGraphCredential(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('mailboxes/microsoft/credentials', [ 'json' => [ 'tenant_id' => 'bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32', 'client_id' => 'e1c6ac69-480e-4a17-9b85-3467967bd179', 'client_secret' => 'your-client-secret', 'secret_expires_at' => '2027-11-28T23:59:59Z', 'name' => 'MS Graph app', ], ]); 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 The credential was created. ```json { "credential_id": 22334455, "tenant_id": "bbfb2db7-aaf4-4f7c-9dc2-2c9160d1cb32", "client_id": "e1c6ac69-480e-4a17-9b85-3467967bd179", "secret_expires_at": "2027-11-28T23:59:59Z", "name": "MS Graph app" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `credential_id` | integer | Created credential ID | | `tenant_id` | string | Microsoft Entra tenant ID | | `client_id` | string | Microsoft Entra application client ID | | `secret_expires_at` | string | Client secret expiration date in ISO 8601 format | | `name` | string | Credential name. | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx). ```json { "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. ```json { "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 | Invalid request or malformed request syntax. The request can also fail when the body is valid JSON, but the credential cannot be saved. ```json { "message": "INVALID_CLIENT_SECRET" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `message` | string | Validation error message. Possible values: `Missing tenantId` - `tenant_id` is missing`Missing clientId` - `client_id` is missing`Missing client secret` - `client_secret` is missing`MISSING_SECRET_EXPIRES_AT` - `secret_expires_at` is missing`SECRET_EXPIRES_AT_NOT_IN_FUTURE` - `secret_expires_at` is not in the future`INVALID_CLIENT_SECRET` - Microsoft Graph rejected the provided credential | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Add Microsoft mailboxes in bulk Connect one or more Microsoft mailboxes with Microsoft Graph app-only authentication. Use a `credential_id` from [List credentials](get-credentials.mdx), or omit it only when your account has exactly one valid Microsoft Graph credential. ## Request ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/mailboxes/microsoft/bulk ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "auth_type": "APP_ONLY", "credential_id": 22334455, "accounts": [ { "mailbox_email": "jim@example.com", "display_name": "Jimothy H" }, { "mailbox_email": "michael@example.com", "display_name": "Michael Scarn" } ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `auth_type` | string | No | Authentication type. Defaults to `APP_ONLY`; only `APP_ONLY` is supported | | `credential_id` | integer | No | Microsoft Graph credential ID. Can be ommited only when your account has exactly one valid Microsoft Graph credential | | `accounts` | array | Yes | Microsoft mailboxes to connect | |   └─`mailbox_email` | string | Yes | Mailbox email address | |   └─`display_name` | string | No | Display name used for the mailbox | ### Request samples #### Connect Microsoft mailboxes ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/bulk" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "auth_type": "APP_ONLY", "credential_id": 22334455, "accounts": [ { "mailbox_email": "jim@example.com", "display_name": "Jimothy H" }, { "mailbox_email": "michael@example.com", "display_name": "Michael Scarn" } ] }' ``` ```python import requests def connect_microsoft_mailboxes(): url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/bulk" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json", } payload = { "auth_type": "APP_ONLY", "credential_id": 22334455, "accounts": [ { "mailbox_email": "jim@example.com", "display_name": "Jimothy H", }, { "mailbox_email": "michael@example.com", "display_name": "Michael Scarn", }, ], } response = requests.post(url, headers=headers, json=payload) if response.status_code == 201: print(response.json()) else: print("Request failed:", response.status_code, response.text) if __name__ == "__main__": connect_microsoft_mailboxes() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { connectMicrosoftMailboxes(); } public static void connectMicrosoftMailboxes() { try { String url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/bulk"; String jsonData = """ { "auth_type": "APP_ONLY", "credential_id": 22334455, "accounts": [ { "mailbox_email": "jim@example.com", "display_name": "Jimothy H" }, { "mailbox_email": "michael@example.com", "display_name": "Michael Scarn" } ] } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 201) { System.out.println(response.body()); } else { System.err.println("Request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function connectMicrosoftMailboxes() { const url = "https://api.woodpecker.co/rest/v2/mailboxes/microsoft/bulk"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { auth_type: "APP_ONLY", credential_id: 22334455, accounts: [ { mailbox_email: "jim@example.com", display_name: "Jimothy H" }, { mailbox_email: "michael@example.com", display_name: "Michael Scarn" } ] }; try { const response = await axios.post(url, data, { headers }); if (response.status === 201) { console.log(response.data); } else { console.error("Request failed:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } connectMicrosoftMailboxes(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('mailboxes/microsoft/bulk', [ 'json' => [ 'auth_type' => 'APP_ONLY', 'credential_id' => 22334455, 'accounts' => [ [ 'mailbox_email' => 'jim@example.com', 'display_name' => 'Jimothy H', ], [ 'mailbox_email' => 'michael@example.com', 'display_name' => 'Michael Scarn', ], ], ], ]); 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 All submitted mailboxes were connected. ```json [ { "success": true, "id": 444444, "email": "jim@example.com", "name": "Jimothy H", "deliverability": { "spf_correct": true, "dkim_correct": true }, "error": null } ] ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `success` | boolean | Whether the mailbox was connected | | `id` | integer | Connected mailbox ID | | `email` | string | Mailbox email address | | `name` | string/null | Mailbox display name | | `deliverability` | object | Deliverability check result | |   └─`spf_correct` | boolean | Whether SPF is configured correctly | |   └─`dkim_correct` | boolean | Whether DKIM is configured correctly | | `error` | string/null | Per-mailbox error code | At least one mailbox could not be connected. Check each response item to see which mailboxes succeeded. ```json [ { "success": true, "id": 444444, "email": "jim@example.com", "name": "Jimothy H", "deliverability": { "spf_correct": true, "dkim_correct": true }, "error": null }, { "success": false, "id": null, "email": "michael@example.com", "name": "Michael Scarn", "deliverability": null, "error": "WRONG_ALIAS" } ] ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `success` | boolean | Whether the mailbox was connected | | `id` | integer/null | Connected mailbox ID, or `null` when the mailbox failed | | `email` | string | Mailbox email address | | `name` | string/null | Mailbox display name | | `deliverability` | object/null | Deliverability check result, or `null` when the mailbox failed | |   └─`spf_correct` | boolean | Whether SPF is configured correctly | |   └─`dkim_correct` | boolean | Whether DKIM is configured correctly | | `error` | string/null | Per-mailbox error code | Invalid or empty JSON request body. ```text Status: 400 Body: none ``` An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx). ```json { "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. ```json { "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 | Unprocessable Entity. The submitted mailbox batch cannot be processed. ```json { "message": "INVALID_CREDENTIAL_ID" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `message` | string | Validation error code. Possible values: `UNSUPPORTED_AUTH_TYPE` - `auth_type` is not supported`MISSING_CREDENTIAL_ID` - `credential_id` is required for this request`MULTIPLE_VALID_CREDENTIALS` - more than one valid credential exists, so `credential_id` must be provided`INVALID_CREDENTIAL_ID` - the provided credential does not exist or cannot be used`MISSING_ACCOUNTS` - the `accounts` array is missing or empty`INVALID_MAILBOX_EMAIL` - at least one submitted mailbox email is invalid | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Add mailboxes in bulk Connect one or multiple mailboxes to your account by providing SMTP/IMAP credentials along with optional sending configurations. This endpoint enables you to set up the connection but you can also specify additional settings, including daily sending limits, tracking domains or footers. You can use this endpoint to connect Gmail and Outlook mailboxes in bulk as well, provided you are using dedicated app passwords instead of regular credentials. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The request body contains a `mailboxes` array where each object represents a mailbox with its SMTP/IMAP connection credentials and optional settings (like sending limits, tracking domains, and notification preferences). You can provide one or multiple mailbox configurations in a single request. :::info You can connect up to 200 mailboxes in a single request. ::: ```json { "mailboxes": [ { "smtp_email": "smtp@mail.com", "smtp_login": "smtp-username", "smtp_password": "secret-smtp-password", "smtp_server": "smtp.server.io", "smtp_port": 465, "smtp_from_name": "smtp-from-name", "imap_email": "imap@mail.com", "imap_password": "secret-imap-password", "imap_server": "imap.server.io", "imap_port": 993, "footer": "
Best regards,
John
", "bcc": "email-to@bcc.com", "open_tracking_domain": "open-domain.com", "click_tracking_domain": "click-domain.com", "unsubscribe_tracking_domain": "unsubscribe-domain.com", "sending_wait_time_from": 10, "sending_wait_time_to": 20, "daily_sending_limit": 100 } ], "completion_notification_types": ["MAIL", "IN_APP"] } ``` ```json { "mailboxes": [ { "smtp_email": "jared@getpiedpiper.com", "smtp_password": "secret-smtp-password", "smtp_server": "smtp.server.io", "smtp_port": 465, "imap_email": "jared@getpiedpiper.com", "imap_password": "secret-imap-password", "imap_server": "imap.server.io", "imap_port": 993 } ] } ``` #### Body schema | Field | Type | Required | Description | |---------|------|-------------|----------| | `mailboxes` | array | Yes | Array of mailbox configurations | | └─`smtp_email` | string | Yes | Email address of the sending mail | | └─`smtp_login` | string | No | Username for SMTP authentication. Use if it's different than the email | | └─`smtp_password` | string | Yes | Password for SMTP authentication | | └─`smtp_server` | string | Yes | SMTP server hostname | | └─`smtp_port` | integer | Yes | SMTP server port number | | └─`smtp_from_name` | string | No | Sender's display name shown to recipients | | └─`imap_email` | string | Yes | Email address of the receiving mail | | └─`imap_password` | string | Yes | Password for IMAP authentication | | └─`imap_server` | string | Yes | IMAP server hostname | | └─`imap_port` | integer | Yes | IMAP server port number | | └─`footer` | string | No | Signature added to sent emails, in HTML format. Supports the `{{UNSUBSCRIBE}}` snippet, which generates an unsubscribe link. Remember to wrap it in an `` tag | | └─`bcc` | string | No | Email address to be added as BCC in all outgoing emails. Useful with CRMs | | └─`open_tracking_domain` | string | No | Custom domain used to track email opens | | └─`click_tracking_domain` | string | No | Custom domain used to track link clicks | | └─`unsubscribe_tracking_domain` | string | No | Custom domain handling unsubscribe requests | | └─`sending_wait_time_from` | integer | No* | Minimum pause between emails in seconds (range: 10-9999) | | └─`sending_wait_time_to` | integer | No* | Maximum pause between emails in seconds (range: 20-9999) | | └─`daily_sending_limit` | integer | No | Maximum number of sent emails allowed per day (range: 1-5500) | | `completion_notification_types` | string[] | No | How to notify you when batch processing completes: via email `MAIL` and/or in-app notification `IN_APP` | \*Note: If either `sending_wait_time_from` or `sending_wait_time_to` is provided, both become required and `from` must be less than `to`. ### Request samples #### Connect a mailbox ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "mailboxes": [ { "smtp_email": "jared@getpiedpiper.com", "smtp_password": "secret-smtp-password", "smtp_server": "smtp.server.io", "smtp_port": 465, "imap_email": "jared@getpiedpiper.com", "imap_password": "secret-imap-password", "imap_server": "imap.server.io", "imap_port": 993 } ] }' ``` ```Python import requests def connect_mailboxes(): url = "https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "mailboxes": [ { "smtp_email": "jared@getpiedpiper.com", "smtp_password": "secret-smtp-password", "smtp_server": "smtp.server.io", "smtp_port": 465, "imap_email": "jared@getpiedpiper.com", "imap_password": "secret-imap-password", "imap_server": "imap.server.io", "imap_port": 993 } ] } try: response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST response:", response.json()) else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") except Exception as e: print("Error:", e) if __name__ == "__main__": connect_mailboxes() ``` ```java import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; public class WoodpeckerApiClient { public static void main(String[] args) { String apiKey = "YOUR_API_KEY"; String endpoint = "https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk"; String jsonInputString = "{" + "\"mailboxes\": [" + "{" + "\"smtp_email\": \"jared@getpiedpiper.com\"," + "\"smtp_password\": \"secret-smtp-password\"," + "\"smtp_server\": \"smtp.server.io\"," + "\"smtp_port\": 465," + "\"imap_email\": \"jared@getpiedpiper.com\"," + "\"imap_password\": \"secret-imap-password\"," + "\"imap_server\": \"imap.server.io\"," + "\"imap_port\": 993" + "}" + "]" + "}"; try { URL url = new URL(endpoint); 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 = jsonInputString.getBytes(StandardCharsets.UTF_8); os.write(input, 0, input.length); } int responseCode = connection.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_CREATED) { BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); String inputLine; StringBuilder response = new StringBuilder(); while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); System.out.println("Response: " + response.toString()); } else { System.out.println("POST request failed with response code: " + responseCode); } connection.disconnect(); } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); const apiKey = "YOUR_API_KEY"; const url = "https://api.woodpecker.co/rest/v2/mailboxes/manual_connection/bulk"; const data = { mailboxes: [ { smtp_email: "jared@getpiedpiper.com", smtp_password: "secret-smtp-password", smtp_server: "smtp.server.io", smtp_port: 465, imap_email: "jared@getpiedpiper.com", imap_password: "secret-imap-password", imap_server: "imap.server.io", imap_port: 993, }, ], }; axios .post(url, data, { headers: { "x-api-key": apiKey, "Content-Type": "application/json", }, }) .then((response) => { console.log("Response:", response.data); }) .catch((error) => { console.error("Error:", error.response ? error.response.data : error.message); }); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('mailboxes/manual_connection/bulk', [ 'json' => [ 'mailboxes' => [ [ 'smtp_email' => 'jared@getpiedpiper.com', 'smtp_password' => 'secret-smtp-password', 'smtp_server' => 'smtp.server.io', 'smtp_port' => 465, 'imap_email' => 'jared@getpiedpiper.com', 'imap_password' => 'secret-imap-password', 'imap_server' => 'imap.server.io', 'imap_port' => 993, ], ], ], ]); 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 The response includes a `batch_id` that allows you to monitor the progress and results of the submitted mailbox connections using [rest/v2/mailboxes/manual_connection/bulk/\{batch_id\}/summary](get-batch-summary.mdx) endpoint. ```json { "batch_id": 123456 } ``` #### Body schema | Field | Type | Description| |-------|----------|--------------------------| | `batch_id` | string | ID of the submitted email batch. Use it to review the connection status. | Invalid request or malformed request syntax. Please review the [request body](#body) and [field requirements](#body-schema). Each validation error will be included in the `details` array. For example, `mailbox[3].daily_sending_limit` indicates the issue encountered while validating the `daily_sending_limit` for the fourth mailbox in your request. Below are a few additional examples. ```json { "code": "VALIDATION_FAILURE", "details": [ "mailbox[0].smtp_password must not be blank", "mailbox[0].imap_port must be provided and in range <1,65535>", "mailbox[0].sending_wait_time_from if provided must be present along with and lower than sending_wait_time_to and be in range <10,9999>", "mailbox[0].sending_wait_time_to if provided must be present along with and higher than sending_wait_time_from and be in range <20,9999>", "mailbox[0].open_tracking_domain if provided must be a valid domain" ] } ``` #### Body schema | Field | Type | Description | |---------|------|-------------| | `code` | string | Code that outlines the request issue. `VALIDATION_FAILURE` or `UNKNOWN` | | `details[]` | array | Array of specific error messages. If the code is `VALIDATION_FAILURE`, every field with an issue will be returned as `mailbox[index].filed message` | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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](#endpoint) ```json { "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 | Conflict ```json { "code": "Conflict", "details": [ "string" ] } ``` Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Update mailbox Update the details of an email account connected to your account. Currently, the property that can be updated is the footer of an SMTP account. ## Request ### Endpoint ``` PATCH https://api.woodpecker.co/rest/v2/mailboxes/{smtp_mailbox_id} ``` You can update an email account using the **SMTP email account ID**. Use the [/mailboxes](get-mailboxes.mdx) endpoint to retrieve a list of your SMTP IDs. ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The request body contains an object with the data to update for an email account. ```json { "footer": "
Best regards,
John
" } ``` #### Body schema | Field | Type | Required | Description | |---------|------|:-------------:|----------| | `footer` | string | Yes | Footer of the email account in HTML format. Use `null` or an empty string to remove the footer. Supports the `{{UNSUBSCRIBE}}` snippet, which generates an unsubscribe link. Remember to wrap it in an `
` tag | ### Request samples #### Update a mailbox ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/mailboxes/{smtp_mailbox_id}" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "footer": "
Best regards,
John
" }' ``` ```Python import requests def update_mailbox(smtp_mailbox_id): url = f"https://api.woodpecker.co/rest/v2/mailboxes/{smtp_mailbox_id}" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "footer": "
Best regards,
John
" } try: response = requests.patch(url, headers=headers, json=payload) if response.status_code == 200: print("PATCH response:", response.json()) else: raise Exception(f"PATCH request failed: {response.status_code}, {response.text}") except Exception as e: print("Error:", e) if __name__ == "__main__": mailbox_id = 9876 update_mailbox(mailbox_id) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int smtpMailboxId = 9876; updateMailboxById(smtpMailboxId); } private static void updateMailboxById(int smtpMailboxId) { String endpoint = "https://api.woodpecker.co/rest/v2/mailboxes/" + smtpMailboxId; String jsonInputString = "{" + "\"footer\": \"
Best regards,
John
\"" + "}"; try { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(endpoint)) .header("x-api-key", API_KEY) .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonInputString)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("PATCH response: " + response.body()); } else { System.err.println("PATCH request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function updateMailboxById(smtpMailboxId) { const apiKey = "YOUR_API_KEY"; const url = `https://api.woodpecker.co/rest/v2/mailboxes/${smtpMailboxId}`; const data = { "footer": "
Best regards,
John
" }; try { const response = await axios.patch(url, data, { headers: { "x-api-key": apiKey, "Content-Type": "application/json", }, }); if (response.status === 200) { console.log("PATCH successful:", response.data); } else { console.error("PATCH failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } updateMailboxById(9876); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $smtpMailboxId = 9876; try { $response = $client->patch("mailboxes/{$smtpMailboxId}", [ 'json' => [ 'footer' => '
Best regards,
John
' ] ]); 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 Request processed successfully. ``` Status: 200 Body: None ``` Invalid request or malformed request syntax. Please review the [request body](#body) and [field requirements](#body-schema). ```json { "code": "MALFORMED_REQUEST", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `details` | array[string]/null | Additional information | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx). ```json { "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 | Mailbox not found. Use the [/mailboxes](get-mailboxes.mdx) endpoint to retrieve a list of your **SMTP IDs**. ```json { "code": "MAILBOX_NOT_FOUND", "details": null } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `details` | array[string]/null | Additional information | Unprocessable request due to the provided IMAP ID. Please use an SMTP ID for this operation. Use the [/mailboxes](get-mailboxes.mdx) endpoint to retrieve a list of your **SMTP IDs** ```json { "code": "IMAP_NOT_ALLOWED", "details": ["Updating IMAP is not allowed"] } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------------------------| | `code` | string | Error code | | `details` | array[string]/null | Additional information | Unexpected error, please try again later ``` Status: 500 Body: none ``` --- ## Get manual tasks Use this endpoint to retrieve a list of all [manual tasks](https://woodpecker.co/help-center/en/articles/5269001) in the authorized account. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/manual_tasks ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Description | | ---------- | -------- | ------------------------------------------------------- | | `limit` | No | Number of retrieved results. Default: 500, max: 500 | ### Request samples #### GET manual tasks ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/manual_tasks?limit=100" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getManualTasks(): url = "https://api.woodpecker.co/rest/v2/manual_tasks?limit=100" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getManualTasks() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getManualTasks(); } public static void getManualTasks() { try { String url = "https://api.woodpecker.co/rest/v2/manual_tasks?limit=100"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getManualTasks() { const url = "https://api.woodpecker.co/rest/v2/manual_tasks?limit=100"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getManualTasks(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('manual_tasks', [ 'query' => [ 'limit' => 100, ], ]); 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 ```json [ { "prospect": { "id": 123456789, "email": "erlich@bachman.com", "first_name": "Erlich", "last_name": "Bachman", "company": null, "website": "https://www.bachmanity.com/", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz/", "tags": "#VISIONARY", "title": "VC Angel", "phone": "", "address": "221 Newell Rd", "city": "Palo Alto", "country": "USA", "snippet1": "Pied Piper board member", "snippet2": "A personalized sentence
in two lines", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "My snippet label": "Pied Piper board member" }, "industry": "Software as a Service", "state": "", "last_contacted": 1735056455867, "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "mydatabase.csv", "interested": null }, "campaign": { "campaign_id": 987654, "campaign_name": "Campaign with tasks", "sent_from_emails": ["jared@piedpiper.com"] }, "task": { "type": "GENERIC", "name": "Reminder", "message": "Send a postcard", "due_date": "2025-05-06T12:59:30.814+0200" } } ] ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `prospect` | object | Contains prospect data | | └─ `id` | integer | Unique identifier for the prospect | | └─ `email` | string | Prospect's email address | | └─ `first_name` | string | Prospect's first name | | └─ `last_name` | string | Prospect's last name | | └─ `company` | null | deprecated | | └─ `website` | string | Prospect's website URL | | └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | | └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | | └─ `title` | string | Prospect's job title | | └─ `phone` | string | Prospect's phone number | | └─ `address` | string | Prospect's address | | └─ `city` | string | Prospect's city | | └─ `country` | string | Prospect's country | | └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | | └─ `snippet_labels` | object | Custom snippet labels | | └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | | └─ `industry` | string | Prospect's industry | | └─ `state` | string | Prospect's state or region | | └─ `last_contacted` | integer | Timestamp (ms) of the last contact date | | └─ `status` | string | Prospect global status | | └─ `in_campaign` | integer | Number of campaigns the prospect is in | | └─ `emails_sent` | integer | Number of emails sent to the prospect | | └─ `imported` | string | Name of the import file | | └─ `interested` | string/null | Prospect's interest status | | `campaign` | object | Contains campaign data | | └─ `campaign_id` | integer | Unique identifier for the campaign | | └─ `campaign_name` | string | Name of the campaign | | └─ `sent_from_emails` | array[string] | List of sender email addresses | | `task` | object | Contains task data | | └─ `type` | string | Task type. Available types: `GENERIC`, `CALL`, `SMS`, `LINKEDIN` | | └─ `name` | string | Task name | | └─ `message` | string | Task message or description | | └─ `due_date` | string | Task due date in ISO 8601 format | ```json { "message": "There are no manual tasks created in your campaigns." } ``` Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Value of limit is incorrect.", "timestamp": "2025-02-20 10:01:16" } ``` #### 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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Delete prospects :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: Delete prospects from your account, either globally (from the global prospect list) or locally (from specific campaigns). A prospect can exist in multiple campaigns but is always part of the global list. Deleting a prospect globally also removes them from any associated campaigns, while removing them from specific campaigns does not affect their presence in the global database. The maximum length of the request URL is 4100 characters, approximately 360 prospects. ## Request ### Endpoint ``` DELETE https://api.woodpecker.co/rest/v1/prospects?id={prospect_id} ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters The endpoint requires the `id` parameter to specify which prospects to delete. The optional `campaigns_id` parameter allows targeting specific campaigns instead of deleting prospects globally. | Parameter | Required | Description | | --------- | --------- | ------------------------------------------ | | `id` | Yes | Comma-separated list of prospect IDs to delete | | `campaigns_id` | No | Comma-separated list of campaigns from which the specified prospects should be removed. | ### Request samples #### Delete two prospects globally ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v1/prospects?id={prospect_id1},{prospect_id2}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def deleteProspects(prospect_ids): ids_param = ",".join(str(pid) for pid in prospect_ids) url = f"https://api.woodpecker.co/rest/v1/prospects?id={ids_param}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.delete(url, headers=headers) if response.status_code == 200: print("DELETE successful — prospects deleted.") else: print("DELETE failed with status:", response.status_code) if __name__ == "__main__": deleteProspects([101, 202]) # Example prospect IDs ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { deleteProspects(101, 202); // Example prospect IDs } public static void deleteProspects(int... ids) { try { StringBuilder idParam = new StringBuilder(); for (int i = 0; i < ids.length; i++) { idParam.append(ids[i]); if (i < ids.length - 1) { idParam.append(","); } } String url = "https://api.woodpecker.co/rest/v1/prospects?id=" + idParam.toString(); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("DELETE successful — prospects deleted."); } else { System.err.println("DELETE failed with status: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function deleteProspects(prospectIds) { const idParam = prospectIds.join(","); const url = `https://api.woodpecker.co/rest/v1/prospects?id=${idParam}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.delete(url, { headers: headers }); if (response.status === 200) { console.log("DELETE successful — prospects deleted."); } else { console.error("DELETE failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } deleteProspects([101, 202]); // Example prospect IDs ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $prospectIds = '{prospect_id1},{prospect_id2}'; try { $response = $client->delete("prospects", [ 'query' => [ 'id' => $prospectIds, ], ]); 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 Request processed successfully. If the specified prospects existed globally or in the given campaigns, they were removed. ``` Status: 200 Body: None ``` An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please review the request parameters and try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Get prospect responses Retrieve a list of responses from a specified prospect. You can get a list of all responses or filter them by campaign. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/prospects/{prospect_id}/responses ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Description | | ------------- | -------- | ------------------------------------------------------------------------------------------- | | `campaign_id` | No | One or more campaign IDs separated by commas. Used to filter results by specific campaigns. | ### Request samples #### Retrieve all responses from a specified prospect ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/prospects/{prospect_id}/responses" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getProspectResponses(prospect_id): url = f"https://api.woodpecker.co/rest/v2/prospects/{prospect_id}/responses" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getProspectResponses(7890) # Example prospect ID ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int prospectId = 7890; // Example prospect ID getProspectResponses(prospectId); } public static void getProspectResponses(int prospectId) { try { String url = "https://api.woodpecker.co/rest/v2/prospects/" + prospectId + "/responses"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getProspectResponses(prospectId) { const url = `https://api.woodpecker.co/rest/v2/prospects/${prospectId}/responses`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getProspectResponses(7890); // Example prospect ID ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $prospectId = '{prospect_id}'; try { $response = $client->get("prospects/{$prospectId}/responses"); 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 A list of responses from a specified prospect sorted by `delivered` ascending. If the prospect exists in your database but has not replied to an email, or if their response was deleted, the `responses[]` array will be empty ```json { "prospect_id": 123456789, "email": "jared.dunn@piedpiper.com", "responses": [ { "response_id": 147258369, "campaign_id": 17654321, "step": 2, "campaign_email_sent": 2, "subject": "Re: What about Pied Piper?", "message": "
This is a reply in HTML format.
", "delivered": "2024-11-09T14:29:06", "secondary_prospect_email": "erlich@bachmanity.com" }, { "response_id": 258369147, "campaign_id": 1957395, "step": 1, "campaign_email_sent": 1, "subject": "Re: Did you hear about world's best startup, Aviato?", "message": "
Yes and I would like to hear more!
", "delivered": "2024-12-03T07:29:06", "secondary_prospect_email": null } ] } ``` ### Response body schema | Field | Type | Description | |-----------|----------|----------------| | `prospect_id` | integer | Unique ID of the prospect | | `email` | string | Email address of the prospect | | `responses`| array | Array of email responses from the prospect| | └─ `response_id` | integer | Unique ID of the response| | └─ `campaign_id` | integer | ID of the campaign associated to a response | | └─ `step` | integer | Campaign step number that triggered the response | | └─ `campaign_email_sent` | integer | Number of campaign emails sent before this response | | └─ `subject` | string | Subject line of the response | | └─ `message` | string | Body of the response email in HTML format | | └─ `delivered` | datetime | Timestamp when the response was received. `YYYY-MM-DDTHH:MM:SS` Europe/Warsaw time | | └─ `secondary_prospect_email` | string / null | Email address of the respondent if the response came from a different email address than the prospect's | Invalid request parameters or malformed request syntax ```json { "title": "Bad Request", "status": 400, "detail": "Value of campaign_id is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 | 1. Prospect ID doesn't exist in your database ``` Status: 404 Body: none ``` 2. Incorrect URL ```json { "title": "Not Found", "status": 404, "detail": "Requested resource does not exist", "timestamp": "2025-03-05 17:57:00" } ``` Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Add prospects to a campaign :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: This endpoint allows you to add one or multiple prospects to a **campaign prospect list** in a single request. Any prospect whose current global status is not `ACTIVE` (e.g., `REPLIED`, `BOUNCED`) will be automatically rejected unless overridden using the 'force' parameter. Duplicate records will return an appropriate message. * You can add an existing prospect from the global prospect list to a campaign without modifying their data (snippets). To update their information, see [this guide](POST-update-prospects-campaign.mdx) * You can add a prospect directly to a campaign - this will also create a record in the global prospect list. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v1/add_prospects_campaign ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The example below shows all available fields. The `prospects[].email` and `campaign.campaign_id` fields are required, while all other fields are optional. If omitted, these fields will remain blank for new prospects. For existing prospects in your global database, their stored data (snippets) will be used. :::tip Use HTML-formatted content, such as a paragraph, sentence, or full message, as snippet values to personalize your campaign content ::: :::info You can add up to 20 000 prospects per request ::: ```json { "campaign": { "campaign_id": 1234567, "send_after": "2025-04-01T00:01:01-0000" }, "force": false, "file_name": "API import YYYY-MM-DD", "prospects": [ { "email": "erlich@bachman.com", "status": "ACTIVE", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "http://www.bachmanity.com/", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz/", "tags": "#VC", "title": "VC Angel", "phone": "111222333", "address": "221 Newell Rd", "city": "Palo Alto", "state": "California", "country": "USA", "industry": "Software as a Service", "snippet1": "Pied Piper board member", "snippet2": "A personalized sentence
in two lines", "snippet3": "string", "snippet4": "string", "snippet5": "string", "snippet6": "string", "snippet7": "string", "snippet8": "string", "snippet9": "string", "snippet10": "string", "snippet11": "string", "snippet12": "string", "snippet13": "string", "snippet14": "string", "snippet15": "string" } ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|---------|-------------| | `campaign` | object | Yes | Contains campaign data | |   └─`campaign_id` | integer | Yes | Campaign ID to which prospects will be added | |   └─`send_after` | string | No | The earliest date and time prospects can be contacted. Use `%2B` for `+` in the timezone (ISO 8601-like format) | | `force` | boolean | No | Use with caution. Whether to add prospects to a campaign, even if their global status is other than `ACTIVE`. If `true`, prospects may be contacted again, even if they have responded in another campaign or opted out. | | `file_name` | string | No | Name of the import batch, visible in the `imported` column | | `[].prospects` | object | Yes | Contains prospect data, there can be multiple prospects | |   └─ `email` | string | Yes | Prospect's email address | |   └─ `status` | string | No | Prospect's status. By default the status is set to `ACTIVE`. Other available: `PAUSED`, `TO-REVIEW`, `TO-CHECK`; available with `force`: `BLACKLIST`, `BOUNCED`, `INVALID`, `REPLIED` | |   └─ `first_name` | string | No | Prospect's first name | |   └─ `last_name` | string | No | Prospect's last name | |   └─ `company` | string | No | Prospect's company name | |   └─ `website` | string | No | Prospect's website URL | |   └─ `linkedin_url` | string | No | Prospect's LinkedIn profile URL | |   └─ `tags` | string | No | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | No | Prospect's job title | |   └─ `phone` | string | No | Prospect's phone number | |   └─ `address` | string | No | Prospect's address | |   └─ `city` | string | No | Prospect's city | |   └─ `country` | string | No | Prospect's country | |   └─ `snippet` | string | No | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `industry` | string | No | Prospect's industry | |   └─ `state` | string | No | Prospect's state or region | ### Request samples #### Add prospects to campaign The example below showcases how to add multiple prospects only with selected snippets. ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v1/add_prospects_campaign" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "campaign": { "campaign_id": 1234567 }, "prospects": [ { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company": "Pied Piper", "snippet1": "Custom snippet value" }, { "email": "erlich@bachman.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Aviato", "snippet1": "Custom snippet value" } ] }' ``` ```Python import requests def addProspectsToCampaign(): url = "https://api.woodpecker.co/rest/v1/add_prospects_campaign" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "campaign": { "campaign_id": 1234567 }, "prospects": [ { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company": "Pied Piper", "snippet1": "Custom snippet value" }, { "email": "erlich@bachman.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Aviato", "snippet1": "Custom snippet value" } ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": addProspectsToCampaign() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { addProspectsToCampaign(); } public static void addProspectsToCampaign() { try { String url = "https://api.woodpecker.co/rest/v1/add_prospects_campaign"; String jsonData = "{" + "\"campaign\": { \"campaign_id\": 1234567 }," + "\"prospects\": [" + "{" + "\"email\": \"jared@piedpiper.com\"," + "\"first_name\": \"Jared\"," + "\"last_name\": \"Dunn\"," + "\"company\": \"Pied Piper\"," + "\"snippet1\": \"Custom snippet value\"" + "}," + "{" + "\"email\": \"erlich@bachman.com\"," + "\"first_name\": \"Erlich\"," + "\"last_name\": \"Bachman\"," + "\"company\": \"Aviato\"," + "\"snippet1\": \"Custom snippet value\"" + "}" + "]" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function addProspectsToCampaign() { const url = "https://api.woodpecker.co/rest/v1/add_prospects_campaign"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { campaign: { campaign_id: 1234567 }, prospects: [ { email: "jared@piedpiper.com", first_name: "Jared", last_name: "Dunn", company: "Pied Piper", snippet1: "Custom snippet value" }, { email: "erlich@bachman.com", first_name: "Erlich", last_name: "Bachman", company: "Aviato", snippet1: "Custom snippet value" } ] }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } addProspectsToCampaign(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('add_prospects_campaign', [ 'json' => [ 'campaign' => [ 'campaign_id' => 1234567, ], 'prospects' => [ [ 'email' => 'jared@piedpiper.com', 'first_name' => 'Jared', 'last_name' => 'Dunn', 'company' => 'Pied Piper', 'snippet1' => 'Custom snippet value', ], [ 'email' => 'erlich@bachman.com', 'first_name' => 'Erlich', 'last_name' => 'Bachman', 'company' => 'Aviato', 'snippet1' => 'Custom snippet value', ], ], ], ]); 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 All prospects have been added to the campaign prospect list. The first prospect already existed in the global prospect list, while the second was newly added. ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456, "prospect": "DUPLICATE" }, { "email": "erlich@bachman.com", "id": 1091123457 } ], "status": { "status": "OK", "code": "OK", "msg": "OK" } } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of prospects added to the campaign prospect list | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer | Unique ID assigned to the prospect | |   └─`[].prospect` | string/null | `DUPLICATE`. Indicates that the prospect already exists in the global prospect list but does not prevent them from being added to a campaign | | `status` | object | Object containing the status details of the request | |   └─`status` | string | General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | Some of the prospects were added. Any duplicates or prospects that could not be added are returned with an appropriate error description. Prospects that return `prospect_campaign` are not considered as an error. ```json { "prospects": [ { "email": "erlich.b@bachman.com", "id": 1091123458 }, { "email": "jared@piedpiper.com", "id": 1091123456, "prospect": "DUPLICATE", "prospect_campaign": "DUPLICATE" }, { "email": "erlichbachman.com", "status": "ERROR", "code": "E_EMAIL", "msg": "This looks like invalid email format: erlichbachman.com" } ], "status": { "status": "OK", "code": "OK", "msg": "OK" } } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of processed prospects | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer/null | Unique ID assigned to the prospect if successfully added. For duplicates, returns the ID of the existing prospect | |   └─`[].prospect` | string/null | `DUPLICATE`. Indicates that the prospect already exists in the global prospect list but does not prevent them from being added to a campaign | |   └─`[].prospect_campaign` | string/null | `DUPLICATE`. Indicates that the prospect already exists in this campaign's prospect list. Prospect remains unmodified | |   └─`[].status` | string/null | `ERROR`. Returned if the prospect has not been added | |   └─`[].code` | string/null | Code indicating the error category. Returned if the prospect has not been added | |   └─`[].msg` | string/null | Descriptive error message. Returned if the prospect has not been added | | `status` | object | Object containing the status details of the request | |   └─`status` | string | General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | None of the requested prospects have been added to the prospect list. For `Status is other than ACTIVE.` error message, please refer to `force` in the [request body](#body) ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456, "prospect": "DUPLICATE", "status": "ERROR", "code": "E_INV_STATUS", "msg": "Status is other than ACTIVE." }, { "email": "erlichbachman.com", "status": "ERROR", "code": "E_EMAIL", "msg": "This looks like invalid email format: erlichbachman.com" } ], "status": { "status": "ERROR", "code": "E_INV_STATUS", "msg": "Status is other than ACTIVE." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of processed prospects | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer/null | Unique ID assigned to the prospect if successfully added. For duplicates, returns the ID of the existing prospect | |   └─`[].prospect` | string/null | `DUPLICATE`. Indicates that the prospect already exists in the global prospect list but does not prevent them from being added to a campaign | |   └─`[].prospect_campaign` | string/null | `DUPLICATE`. Indicates that the prospect already exists in this campaign's prospect list. Prospect remains unmodified | |   └─`[].status` | string/null | `ERROR`. Returned if the prospect has not been added | |   └─`[].code` | string/null | Code indicating the error category. Returned if the prospect has not been added | |   └─`[].msg` | string/null | Descriptive error message. Returned if the prospect has not been added | | `status` | object | Object containing the status details of the request | |   └─`status` | string | `ERROR`. General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | Invalid request or malformed request syntax. ```json { "status": { "status": "ERROR", "code": "E_PARSER_ERROR" | "E_RECORD_NOT_FOUND", "msg": "Invalid request body." | "Campaign not found." | "Campaign ID not found." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Add prospects to database :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: This endpoint allows you to add one or multiple prospects to your **global prospect list** in a single request. Prospects will not be enrolled in any campaign but will be available in your account for further actions. If a prospect already exists, their data will remain unchanged. To update prospect data, see [this guide](POST-update-prospect-list.mdx). ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v1/add_prospects_list ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The example below shows all available fields. The `prospects[].email` field is required, while all other fields are optional. If omitted, they will remain blank. :::tip Use HTML-formatted content, such as a paragraph, sentence, or full message, as snippet values to personalize your campaign content ::: :::info You can add up to 20 000 prospects per request ::: ```json { "file_name": "API import YYYY-MM-DD", "prospects": [ { "email": "erlich@bachman.com", "status": "ACTIVE", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "http://www.bachmanity.com/", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz/", "tags": "#VC", "title": "VC Angel", "phone": "111222333", "address": "221 Newell Rd", "city": "Palo Alto", "state": "California", "country": "USA", "industry": "Software as a Service", "snippet1": "Pied Piper board member", "snippet2": "A personalized sentence
in two lines", "snippet3": "string", "snippet4": "string", "snippet5": "string", "snippet6": "string", "snippet7": "string", "snippet8": "string", "snippet9": "string", "snippet10": "string", "snippet11": "string", "snippet12": "string", "snippet13": "string", "snippet14": "string", "snippet15": "string" } ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|---------|-------------| | `[].prospects` | object | Yes | Contains prospect data, there can be multiple prospects | |   └─ `email` | string | Yes | Prospect's email address | |   └─ `status` | string | No | Prospect's status. By default the status is set to `ACTIVE`. Other available: `BLACKLIST`, `BOUNCED`, `INVALID`, `REPLIED` | |   └─ `first_name` | string | No | Prospect's first name | |   └─ `last_name` | string | No | Prospect's last name | |   └─ `company` | string | No | Prospect's company name | |   └─ `website` | string | No | Prospect's website URL | |   └─ `linkedin_url` | string | No | Prospect's LinkedIn profile URL | |   └─ `tags` | string | No | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | No | Prospect's job title | |   └─ `phone` | string | No | Prospect's phone number | |   └─ `address` | string | No | Prospect's address | |   └─ `city` | string | No | Prospect's city | |   └─ `country` | string | No | Prospect's country | |   └─ `snippet` | string | No | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `industry` | string | No | Prospect's industry | |   └─ `state` | string | No | Prospect's state or region | | `file_name` | string | No | Name of the import batch, visible in the `imported` column | ### Request samples #### Add prospects The example below showcases how to add multiple prospects only with selected snippets. ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v1/add_prospects_list" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "prospects": [ { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company": "Pied Piper", "snippet1": "Custom snippet value" }, { "email": "erlich@bachman.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Aviato", "snippet1": "Custom snippet value" } ] }' ``` ```Python import requests def addProspectsToList(): url = "https://api.woodpecker.co/rest/v1/add_prospects_list" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "prospects": [ { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company": "Pied Piper", "snippet1": "Custom snippet value" }, { "email": "erlich@bachman.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Aviato", "snippet1": "Custom snippet value" } ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": addProspectsToList() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { addProspectsToList(); } public static void addProspectsToList() { try { String url = "https://api.woodpecker.co/rest/v1/add_prospects_list"; String jsonData = "{" + "\"prospects\": [" + "{" + "\"email\": \"jared@piedpiper.com\"," + "\"first_name\": \"Jared\"," + "\"last_name\": \"Dunn\"," + "\"company\": \"Pied Piper\"," + "\"snippet1\": \"Custom snippet value\"" + "}," + "{" + "\"email\": \"erlich@bachman.com\"," + "\"first_name\": \"Erlich\"," + "\"last_name\": \"Bachman\"," + "\"company\": \"Aviato\"," + "\"snippet1\": \"Custom snippet value\"" + "}" + "]" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function addProspectsToList() { const url = "https://api.woodpecker.co/rest/v1/add_prospects_list"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { prospects: [ { email: "jared@piedpiper.com", first_name: "Jared", last_name: "Dunn", company: "Pied Piper", snippet1: "Custom snippet value" }, { email: "erlich@bachman.com", first_name: "Erlich", last_name: "Bachman", company: "Aviato", snippet1: "Custom snippet value" } ] }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } addProspectsToList(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('add_prospects_list', [ 'json' => [ 'prospects' => [ [ 'email' => 'jared@piedpiper.com', 'first_name' => 'Jared', 'last_name' => 'Dunn', 'company' => 'Pied Piper', 'snippet1' => 'Custom snippet value', ], [ 'email' => 'erlich@bachman.com', 'first_name' => 'Erlich', 'last_name' => 'Bachman', 'company' => 'Aviato', 'snippet1' => 'Custom snippet value', ], ], ], ]); 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 All prospects have been added to the prospect list. ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456 }, { "email": "erlich@bachman.com", "id": 1091123457 } ], "status": { "status": "OK", "code": "OK", "msg": "OK" } } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of prospects added to the prospect list | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer | Unique ID assigned to the prospect | | `status` | object | Object containing the status details of the request | |   └─`status` | string | General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | At least one prospect has been added to the prospect list. Any duplicates or prospects that could not be added are returned with an appropriate error description. ```json { "prospects": [ { "email": "erlich.bachman@bachman.com", "id": 1091123458 }, { "email": "jared@piedpiper.com", "id": 1091123456, "status": "ERROR", "code": "E_DUPLICATE", "msg": "Duplicate. The prospect has been added to your prospect base before." }, { "email": "erlichbachman.com", "status": "ERROR", "code": "E_EMAIL", "msg": "This looks like invalid email format: erlichbachman.com" } ], "status": { "status": "OK", "code": "OK", "msg": "OK" } } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of processed prospects | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer/null | Unique ID assigned to the prospect if successfully added. For duplicates, returns the ID of the existing prospect | |   └─`[].status` | string/null | `ERROR`. Returned if the prospect has not been added | |   └─`[].code` | string/null | Code indicating the error category. Returned if the prospect has not been added | |   └─`[].msg` | string/null | Descriptive error message. Returned if the prospect has not been added | | `status` | object | Object containing the status details of the request | |   └─`status` | string | General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | None of the requested prospects have been added to the prospect list ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456, "status": "ERROR", "code": "E_DUPLICATE", "msg": "Duplicate. The prospect has been added to your prospect base before." }, { "email": "erlichbachman.com", "status": "ERROR", "code": "E_EMAIL", "msg": "This looks like invalid email format: erlichbachman.com" } ], "status": { "status": "ERROR", "code": "E_DUPLICATE", "msg": "Duplicate. The prospect has been added to your prospect base before." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of processed prospects | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer/null | Unique ID assigned to the prospect if successfully added. For duplicates, returns the ID of the existing prospect | |   └─`[].status` | string/null | `ERROR`. Returned if the prospect has not been added | |   └─`[].code` | string/null | Code indicating the error category. Returned if the prospect has not been added | |   └─`[].msg` | string/null | Descriptive error message. Returned if the prospect has not been added | | `status` | object | Object containing the status details of the request | |   └─`status` | string | `ERROR`. General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | Invalid request or malformed request syntax. ```json { "status": { "status": "ERROR", "code": "E_PARSER_ERROR", "msg": "Invalid request body." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Update prospects in database :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: This endpoint allows you to update one or multiple prospects in your **global prospect list** in a single request. You can update their statuses and snippet data. Existing prospects will be updated, while new prospects that do not exist in your database will be added. The key difference between this and [add prospects endpoint](POST-add-prospects-list.mdx) is the `update` property. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v1/add_prospects_list ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The example below shows all available fields. The `update` and `prospects[].email` field is required, while all other fields are optional. If omitted, they will not be updated. :::tip Use HTML-formatted content, such as a paragraph, sentence, or full message, as snippet values to personalize your campaign content ::: :::info You can update up to 20 000 prospects per request ::: ```json { "update": true, "file_name": "API import YYYY-MM-DD", "prospects": [ { "email": "erlich@bachman.com", "status": "ACTIVE", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "http://www.bachmanity.com/", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz/", "tags": "#VC", "title": "VC Angel", "phone": "111222333", "address": "221 Newell Rd", "city": "Palo Alto", "state": "California", "country": "USA", "industry": "Software as a Service", "snippet1": "Pied Piper board member", "snippet2": "A personalized sentence
in two lines", "snippet3": "string", "snippet4": "string", "snippet5": "string", "snippet6": "string", "snippet7": "string", "snippet8": "string", "snippet9": "string", "snippet10": "string", "snippet11": "string", "snippet12": "string", "snippet13": "string", "snippet14": "string", "snippet15": "string" } ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|---------|-------------| | `update` | boolean | Yes | This property has to be set to `true` to update prospects. Otherwise existing prospects will return `E_DUPLICATE` code | | `file_name` | string | No | Name of the import batch, visible in the `imported` column. If not specified, it will clear the existing values | | `[].prospects` | object | Yes | Contains prospect data, there can be multiple prospects | |   └─ `email` | string | Yes | Prospect's email address | |   └─ `status` | string | No | Prospect's status. Available statuses: `ACTIVE`, `BLACKLIST`, `BOUNCED`, `INVALID`, `REPLIED` | |   └─ `first_name` | string | No | Prospect's first name | |   └─ `last_name` | string | No | Prospect's last name | |   └─ `company` | string | No | Prospect's company name | |   └─ `website` | string | No | Prospect's website URL | |   └─ `linkedin_url` | string | No | Prospect's LinkedIn profile URL | |   └─ `tags` | string | No | Tags to add to a prospect. These will be appended to existing tags (do not overwrite). Each tag starts with # and is separated by a space | |   └─ `set_tags` | string | No | Replaces all existing tags for the prospect with the provided ones. Each tag starts with # and is separated by spaces. Send an empty string (`""`) to clear all existing tags | |   └─ `title` | string | No | Prospect's job title | |   └─ `phone` | string | No | Prospect's phone number | |   └─ `address` | string | No | Prospect's address | |   └─ `city` | string | No | Prospect's city | |   └─ `country` | string | No | Prospect's country | |   └─ `snippet` | string | No | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `industry` | string | No | Prospect's industry | |   └─ `state` | string | No | Prospect's state or region | ### Request samples #### Update prospects The example below showcases how to update multiple prospects only with selected snippets. ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v1/add_prospects_list" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "update": true, "prospects": [ { "email": "jared@piedpiper.com", "snippet1": "New snippet value" }, { "email": "erlich@bachman.com", "snippet1": "New snippet value" } ] }' ``` ```Python import requests def updateProspectsInList(): url = "https://api.woodpecker.co/rest/v1/add_prospects_list" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "update": True, "prospects": [ { "email": "jared@piedpiper.com", "snippet1": "New snippet value" }, { "email": "erlich@bachman.com", "snippet1": "New snippet value" } ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": updateProspectsInList() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { updateProspectsInList(); } public static void updateProspectsInList() { try { String url = "https://api.woodpecker.co/rest/v1/add_prospects_list"; String jsonData = "{" + "\"update\": true," + "\"prospects\": [" + "{" + "\"email\": \"jared@piedpiper.com\"," + "\"snippet1\": \"New snippet value\"" + "}," + "{" + "\"email\": \"erlich@bachman.com\"," + "\"snippet1\": \"New snippet value\"" + "}" + "]" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function updateProspectsInList() { const url = "https://api.woodpecker.co/rest/v1/add_prospects_list"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { update: true, prospects: [ { email: "jared@piedpiper.com", snippet1: "New snippet value" }, { email: "erlich@bachman.com", snippet1: "New snippet value" } ] }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } updateProspectsInList(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('add_prospects_list', [ 'json' => [ 'update' => true, 'prospects' => [ [ 'email' => 'jared@piedpiper.com', 'snippet1' => 'New snippet value', ], [ 'email' => 'erlich@bachman.com', 'snippet1' => 'New snippet value', ], ], ], ]); 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 All prospects have been updated. Any prospect that did not previously exist has been added to the prospect list. ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456 }, { "email": "erlich@bachman.com", "id": 1091123457 } ], "status": { "status": "OK", "code": "OK", "msg": "OK" } } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of prospects added to the prospect list | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer | Unique ID of a prospect | | `status` | object | Object containing the status details of the request | |   └─`status` | string | General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | At least one prospect has been updated or added to the prospect list. Any prospects that could not be updated are returned with an appropriate error description. ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456 }, { "email": "erlichbachman.com", "status": "ERROR", "code": "E_EMAIL", "msg": "This looks like invalid email format: erlichbachman.com" } ], "status": { "status": "OK", "code": "OK", "msg": "OK" } } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of processed prospects | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer/null | Unique ID of a prospect | |   └─`[].status` | string/null | `ERROR`. Returned if the prospect has not been updated | |   └─`[].code` | string/null | Code indicating the error category. Returned if the prospect has not been updated | |   └─`[].msg` | string/null | Descriptive error message. Returned if the prospect has not been updated | | `status` | object | Object containing the status details of the request | |   └─`status` | string | General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | None of the requested prospects have been updated ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456, "status": "ERROR", "code": "E_INV_STATUS", "msg": "Status unknown." }, { "email": "erlichbachman.com", "status": "ERROR", "code": "E_EMAIL", "msg": "This looks like invalid email format: erlichbachman.com" } ], "status": { "status": "ERROR", "code": "E_EMAIL", "msg": "Emails could not be added." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of processed prospects | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer/null | Unique ID of a prospect | |   └─`[].status` | string/null | `ERROR`. Returned if the prospect has not been updated | |   └─`[].code` | string/null | Code indicating the error category. Returned if the prospect has not been updated | |   └─`[].msg` | string/null | Descriptive error message. Returned if the prospect has not been updated | | `status` | object | Object containing the status details of the request | |   └─`status` | string | `ERROR`. General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | Invalid request or malformed request syntax. ```json { "status": { "status": "ERROR", "code": "E_PARSER_ERROR", "msg": "Invalid request body." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Update prospects in a campaign :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: This endpoint allows you to update one or multiple prospects in your **campaign prospect list** in a single request. You can update their statuses, interest level, earliest allowed sending date and snippet data. Any prospect whose global status is not `ACTIVE` (e.g., `REPLIED`, `BOUNCED`) will be automatically rejected unless overridden using the 'force' parameter. For updating global snippet values, we recommend using the `rest/v1/add_prospects_list` [endpoint](POST-update-prospect-list.mdx) instead, as it provides clearer response messages. Existing prospects will be updated, while new prospects that do not exist in your database will be added. The key difference between this and [add prospects endpoint](POST-add-prospects-campaign.mdx) is the `update` property. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v1/add_prospects_campaign ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body The example below shows all available fields. The `update`, `prospects[].email` and `campaign.campaign_id` fields are required, while all other fields are optional. If omitted, these fields will remain blank for new prospects. For existing prospects in your global database, their stored data (snippets) will be used. :::tip Use HTML-formatted content, such as a paragraph, sentence, or full message, as snippet values to personalize your campaign content ::: :::info You can update up to 20 000 prospects per request ::: ```json { "campaign": { "campaign_id": 1234567, "send_after": "2025-04-01T00:01:01-0000" }, "update": true, "force": false, "file_name": "API import YYYY-MM-DD", "prospects": [ { "email": "erlich@bachman.com", "status": "ACTIVE", "interested": "INTERESTED", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "http://www.bachmanity.com/", "linkedin_url": "https://www.linkedin.com/in/erlich-bachman-404xyz/", "tags": "#VC", "title": "VC Angel", "phone": "111222333", "address": "221 Newell Rd", "city": "Palo Alto", "state": "California", "country": "USA", "industry": "Software as a Service", "snippet1": "Pied Piper board member", "snippet2": "A personalized sentence
in two lines", "snippet3": "string", "snippet4": "string", "snippet5": "string", "snippet6": "string", "snippet7": "string", "snippet8": "string", "snippet9": "string", "snippet10": "string", "snippet11": "string", "snippet12": "string", "snippet13": "string", "snippet14": "string", "snippet15": "string" } ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|---------|-------------| | `campaign` | object | Yes | Contains campaign data | |   └─`campaign_id` | integer | Yes | Campaign ID to which prospects will be added | |   └─`send_after` | string | No | The earliest date and time prospects can be contacted. Use `%2B` for `+` in the timezone (ISO 8601-like format) | | `update` | boolean | Yes | This property has to be set to `true` to update prospects. Otherwise existing prospects will return `E_DUPLICATE` code | | `force` | boolean | No | Use with caution. Whether to add prospects to a campaign, even if their global status is other than `ACTIVE`. If `true`, prospects may be contacted again, even if they have responded in another campaign or opted out. | | `file_name` | string | No | Name of the import batch, visible in the `imported` column | | `[].prospects` | object | Yes | Contains prospect data, there can be multiple prospects | |   └─ `email` | string | Yes | Prospect's email address | |   └─ `status` | string | No | Prospect's status. Available statuses: `PAUSED`; available with `force`: `BLACKLIST`, `REPLIED`, `INVALID`, `BOUNCED` | |   └─ `interested` | string | No | Prospect's interest level. `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED`, `NOT_MARKED` | |   └─ `first_name` | string | No | Prospect's first name | |   └─ `last_name` | string | No | Prospect's last name | |   └─ `company` | string | No | Prospect's company name | |   └─ `website` | string | No | Prospect's website URL | |   └─ `linkedin_url` | string | No | Prospect's LinkedIn profile URL | |   └─ `tags` | string | No | Tags to add to a prospect. These will be appended to existing tags (do not overwrite). Each tag starts with # and is separated by a space | |   └─ `set_tags` | string | No | Replaces all existing tags for the prospect with the provided ones. Each tag starts with # and is separated by spaces. Send an empty string (`""`) to clear all existing tags | |   └─ `title` | string | No | Prospect's job title | |   └─ `phone` | string | No | Prospect's phone number | |   └─ `address` | string | No | Prospect's address | |   └─ `city` | string | No | Prospect's city | |   └─ `country` | string | No | Prospect's country | |   └─ `snippet` | string | No | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `industry` | string | No | Prospect's industry | |   └─ `state` | string | No | Prospect's state or region | ### Request samples #### Update prospects in campaign The example below showcases how to update multiple prospects only with selected snippets. ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v1/add_prospects_campaign" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "campaign": { "campaign_id": 1234567 }, "update": true, "prospects": [ { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company": "Pied Piper", "snippet1": "Custom snippet value" }, { "email": "erlich@bachman.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Aviato", "snippet1": "Custom snippet value" } ] }' ``` ```Python import requests def updateProspectsInCampaign(): url = "https://api.woodpecker.co/rest/v1/add_prospects_campaign" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "campaign": { "campaign_id": 1234567 }, "update": True, "prospects": [ { "email": "jared@piedpiper.com", "first_name": "Jared", "last_name": "Dunn", "company": "Pied Piper", "snippet1": "Custom snippet value" }, { "email": "erlich@bachman.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Aviato", "snippet1": "Custom snippet value" } ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": updateProspectsInCampaign() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { updateProspectsInCampaign(); } public static void updateProspectsInCampaign() { try { String url = "https://api.woodpecker.co/rest/v1/add_prospects_campaign"; String jsonData = "{" + "\"campaign\": { \"campaign_id\": 1234567 }," + "\"update\": true," + "\"prospects\": [" + "{" + "\"email\": \"jared@piedpiper.com\"," + "\"first_name\": \"Jared\"," + "\"last_name\": \"Dunn\"," + "\"company\": \"Pied Piper\"," + "\"snippet1\": \"Custom snippet value\"" + "}," + "{" + "\"email\": \"erlich@bachman.com\"," + "\"first_name\": \"Erlich\"," + "\"last_name\": \"Bachman\"," + "\"company\": \"Aviato\"," + "\"snippet1\": \"Custom snippet value\"" + "}" + "]" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function updateProspectsInCampaign() { const url = "https://api.woodpecker.co/rest/v1/add_prospects_campaign"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { campaign: { campaign_id: 1234567 }, update: true, prospects: [ { email: "jared@piedpiper.com", first_name: "Jared", last_name: "Dunn", company: "Pied Piper", snippet1: "Custom snippet value" }, { email: "erlich@bachman.com", first_name: "Erlich", last_name: "Bachman", company: "Aviato", snippet1: "Custom snippet value" } ] }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } updateProspectsInCampaign(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('add_prospects_campaign', [ 'json' => [ 'campaign' => [ 'campaign_id' => 1234567, ], 'update' => true, 'prospects' => [ [ 'email' => 'jared@piedpiper.com', 'first_name' => 'Jared', 'last_name' => 'Dunn', 'company' => 'Pied Piper', 'snippet1' => 'Custom snippet value', ], [ 'email' => 'erlich@bachman.com', 'first_name' => 'Erlich', 'last_name' => 'Bachman', 'company' => 'Aviato', 'snippet1' => 'Custom snippet value', ], ], ], ]); 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 All prospects have been updated or added. The first prospect already existed in the campaign prospect database, while the second was new and has been added. ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456, "prospect_campaign": "DUPLICATE" }, { "email": "erlich@bachman.com", "id": 1091123457 } ], "status": { "status": "OK", "code": "OK", "msg": "OK" } } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of prospects added to the campaign prospect list | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer | Unique ID assigned to the prospect | |   └─`[].prospect_campaign` | string/null | `DUPLICATE`. Indicates that the prospect already exists in this campaign's prospect list. Prospect's data has been updated | | `status` | object | Object containing the status details of the request | |   └─`status` | string | General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | Some of the prospects were updated or added. Prospects that could not be updated are returned with an appropriate error description. Prospects that return `prospect_campaign` are not considered as an error. In this scenario, the global snippets of prospect 1091123459 will be updated - unless the response code is 400, in which case they won't be. ```json { "prospects": [ { "email": "erlich.b@bachman.com", "id": 1091123458 }, { "email": "jared@piedpiper.com", "id": 1091123456, "prospect_campaign": "DUPLICATE" }, { "email": "gabe@piedpiper.com", "id": 1091123459, "status": "ERROR", "code": "E_INV_STATUS", "msg": "Status is other than ACTIVE." }, { "email": "erlichbachman.com", "status": "ERROR", "code": "E_EMAIL", "msg": "This looks like invalid email format: erlichbachman.com" } ], "status": { "status": "OK", "code": "OK", "msg": "OK" } } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of processed prospects | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer/null | Unique ID assigned to the prospect if successfully added. For duplicates, returns the ID of the existing prospect | |   └─`[].prospect_campaign` | string/null | `DUPLICATE`. Indicates that the prospect already exists in this campaign's prospect list. Prospect remains unmodified | |   └─`[].status` | string/null | `ERROR`. Returned if the prospect has not been added | |   └─`[].code` | string/null | Code indicating the error category. Returned if the prospect has not been added | |   └─`[].msg` | string/null | Descriptive error message. Returned if the prospect has not been added | | `status` | object | Object containing the status details of the request | |   └─`status` | string | General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | None of the requested prospects have been updated or added to the prospect list. For `Status is other than ACTIVE.` error message, please refer to `force` in the [request body](#body) ```json { "prospects": [ { "email": "jared@piedpiper.com", "id": 1091123456, "status": "ERROR", "code": "E_INV_STATUS", "msg": "Status is other than ACTIVE." }, { "email": "erlichbachman.com", "status": "ERROR", "code": "E_EMAIL", "msg": "This looks like invalid email format: erlichbachman.com" } ], "status": { "status": "ERROR", "code": "E_INV_STATUS", "msg": "Status is other than ACTIVE." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `prospects` | array[object] | An array of processed prospects | |   └─`[].email` | string | Prospect's email | |   └─`[].id` | integer/null | Unique ID assigned to the prospect if successfully added. For duplicates, returns the ID of the existing prospect | |   └─`[].status` | string/null | `ERROR`. Returned if the prospect has not been added | |   └─`[].code` | string/null | Code indicating the error category. Returned if the prospect has not been added | |   └─`[].msg` | string/null | Descriptive error message. Returned if the prospect has not been added | | `status` | object | Object containing the status details of the request | |   └─`status` | string | `ERROR`. General status message | |   └─`code` | string | Code indicating the error category | |   └─`msg` | string | Error message | Invalid request or malformed request syntax. ```json { "status": { "status": "ERROR", "code": "E_PARSER_ERROR" | "E_RECORD_NOT_FOUND", "msg": "Invalid request body." | "Campaign not found." | "Campaign ID not found." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Get prospects in campaign :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: Retrieve a paginated list of prospects enrolled in given campaigns. Each prospect is returned together with their snippet data, **campaign** status, campaign information like campaign name, status, as well as prospect's interest level in this campaign. You can [sort](#sorting-and-pagination) the prospects or [filter](#filtering) them by specific fields and values. If you are looking for: * prospects in your whole account - visit [this guide](get-prospects.mdx) * searching for prospects - visit [this guide](get-search-prospects.mdx) ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v1/prospects?campaigns_id={campaign_ids} ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters #### Sorting and pagination | Parameter | Required | Type | Description | | ------------- | -------- | ----- | -------------------- | | `page` | No | integer | Requested results page (1-based) | | `per_page` | No | integer | Number of records per page. Default: 100, maximum: 1000 | | `sort` | No | string | Order the results based on the specified column. Default: order by ID ascending | For sorting, use `+` before the field name for ascending order and `-` for descending order. To sort by multiple fields, separate them with a comma. To sort prospects by `company` name ascending and `last_contacted` date descending use: `sort=+company,-last_contacted`
Fields available for sorting * id * status * updated * email * last_contacted * last_replied * first_name * last_name * company * organization_id * industry * website * tags * title * phone * address * city * state * country * snipet1 - snipet4 * snippet5 - snippet15 * last_opened - available only with `OPENED` activity paramater * last_clicked - available only with `CLICKED` activity paramater * sent_mails - available only with `OPENED` or `CLICKED` activity paramater
#### Filtering You can filter results by using one or more of the following parameters: | Parameter | Required | Type | Description | | ------------- | -----| -------- | ---------------------------------- | | `campaigns_id` | Yes | integer | Comma-separated list of campaign IDs that prospects are enrolled in | | `id` | No | integer | Comma-separated list of prospect IDs to retrieve | | `status` | No | string | Prospect's **campaign** status: `ACTIVE`, `BOUNCED`, `TO-CHECK`, `TO-REVIEW`, `REPLIED`, `AUTOREPLIED`, `BLACKLIST`, `PAUSED`, `INVALID` | | `contacted` | No | boolean | Whether a prospect has been contacted in the requested campaigns | | `interested` | No | string | Returns prospects only if they have a specific interest level set in any of the campaigns they are enrolled in. Available values: `INTERESTED`, `MAYBE-LATER`, `NOT-INTERESTED`, `NOT-MARKED`. Please mind `_` in the response | | `activity` | No | string | Returns prospects based on their click and open activity. Available values: `OPENED`, `NOT-OPENED`, `CLICKED`, `NOT-CLICKED` | | `diff` | No | string | Return prospects whose activity timestamp is **greater than** the provided date. Uses ISO 8601-like format: `2025-01-15T00:00:00%2B0200`. Available timestamps: `updated`, `last_opened` (only with `OPENED` activity paramater), `last_clicked` (only with `CLICKED` activity paramater) | ### Request samples #### Get prospects in specific campaigns The example below showcases how to fetch last 50 most recently updated prospects, in two given campaigns, with campaign status `REPLIED`. ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v1/prospects?sort=-updated&status=REPLIED&campaigns_id=321654,987654&per_page=50" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getFilteredProspects(): url = "https://api.woodpecker.co/rest/v1/prospects?sort=-updated&status=REPLIED&campaigns_id=321654,987654&per_page=50" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getFilteredProspects() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getFilteredProspects(); } public static void getFilteredProspects() { try { String url = "https://api.woodpecker.co/rest/v1/prospects?sort=-updated&status=REPLIED&campaigns_id=321654,987654&per_page=50"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getFilteredProspects() { const url = "https://api.woodpecker.co/rest/v1/prospects?sort=-updated&status=REPLIED&campaigns_id=321654,987654&per_page=50"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getFilteredProspects(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('prospects', [ 'query' => [ 'sort' => '-updated', 'status' => 'REPLIED', 'campaigns_id' => '321654,987654', 'per_page' => 50, ], ]); 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 headers | Header | Type | Description | | ------------- | -------- | --------------------------------- | | `X-Total-Count` | integer | Total number of prospect objects that match the criteria. | ### Response examples An array of prospect objects. If a prospect exists in multiple requested campaigns, they will be returned as separate, per-campaign, objectes. Some fields will be returned only when the request uses specific paramaters. Such properties are not available in the example below and have a note in the [body schema](#body-schema) ```json [ { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "organization_id": 654321789, "industry": "IT", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "state": "California", "country": "United States", "last_contacted": "2025-03-20T14:32:34+0100", "last replied": "2025-03-21T08:11:35+0100", "updated": "2025-03-21T08:11:35+0100", "encrypted": false, "snipet1": "You are running a successful startup incubator Bachmanity", "snipet2": "", "snipet3": "", "snipet4": "", "snippet1": "", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "custom_snippet": "You are running a successful startup incubator Bachmanity" }, "interested": "MAYBE_LATER", "interest_level": { "level": "MAYBE_LATER", "ai_detected": false }, "campaign_id": 654321, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "followup_after": "2025-03-22T00:00:00+0100", "campaign_status": "RUNNING", "sent_mails": 1, "status": "REPLIED" } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `organization_id` | integer | Unique identifier of a prospect's company | |   └─ `industry` | string | Prospect's industry | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `state` | string | Prospect's state or region | |   └─ `country` | string | Prospect's country | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `last_contacted` | string | Date when the prospect was last contacted in any of the campaigns (ISO 8601 format) | |   └─ `last replied` | string | Date when the prospect last replied to any of the campaigns (ISO 8601 format). Note the missing `_` | |   └─ `updated` | string | Date when the prospect was last updated (ISO 8601 format) | |   └─ `encrypted` | boolean | Whether a prospect is [encrypted](https://woodpecker.co/help-center/en/articles/5258897) | |   └─ `snipet` | string | Legacy. Always equal to the corresponding `snippetX` values. There are 4 snipet fields (`snipet1` to `snipet4`) | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `status` | string | Prospect's **campaign** status | |   └─ `interested` | string/null | Deprecated. Available only if the prospect has an assigned interest level in a given campaign. Available values: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` | |   └─ `interest_level` | object/null | Prospect's Interest Level information. Available only if the prospect has an assigned interest level in a given campaign | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `followup_after` | string/null | Available only if its value is set. The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. | |   └─ `campaign_status` | string | Current status of the campaign. Available values: `RUNNING`, `DRAFT`, `STOPPED`, `PAUSED`, `EDITED`, `COMPLETED` | |   └─ `sent_mails` | integer/null | Available only if a prospect has been contacted. Number of emails sent from the specific campaign the webhook comes from | |   └─ `last_open` | string/null | Available only with `OPENED` activity parameter. Timestamp when the prospect has last opened an email | |   └─ `last_clicked` | string/null | Available only with `CLICKED` activity parameter. Timestamp when the prospect has last clicked a link in an email | |   └─ `click_url` | string/null | Available only with `CLICKED` activity parameter. URL of the clicked link. Each clicked link will result in a separate prospect object | There are no prospects matching your criteria. Please review the request parameters and your prospect database. ```json { "message": "There are no prospects matching the given criteria." } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `message` | string | Descriptive response message | Invalid request or malformed request syntax. ```json { "status": { "status": "ERROR", "code": "E_WRONG_PARAM", "msg": "Wrong param [param_name]=requested_param" | "Unknown param:madeUpParam" | "Wrong param, required activity=OPENED|NOT-OPENED" | "Wrong param, required activity=CLICKED|NOT-CLICKED" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Get a list of prospects :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: Retrieve a paginated list of prospects available in your account. Each prospect is returned together with their snippet data, **global** status, dates like `last_contacted` date. You can [sort](#sorting-and-pagination) the prospects or [filter](#filtering) them by specific fields and values. If you are looking for: * prospects in a specific campaign - visit [this guide](get-prospects-campaign.mdx) * searching for prospects - visit [this guide](get-search-prospects.mdx) ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v1/prospects ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters #### Sorting and pagination | Parameter | Required | Type | Description | | ------------- | -------- | ----- | -------------------- | | `page` | No | integer | Requested results page (1-based) | | `per_page` | No | integer | Number of records per page. Default: 100, maximum: 1000 | | `sort` | No | string | Order the results based on the specified column. Default: order by ID ascending | For sorting, use `+` before the field name for ascending order and `-` for descending order. To sort by multiple fields, separate them with a comma. To sort prospects by `company` name ascending and `last_contacted` date descending use: `sort=+company,-last_contacted`
Fields available for sorting * id * status * updated * email * last_contacted * last_replied * first_name * last_name * company * organization_id * industry * website * tags * title * phone * address * city * state * country * snipet1 - snipet4 * snippet5 - snippet15 * last_opened - available only with `OPENED` activity paramater * last_clicked - available only with `CLICKED` activity paramater * sent_mails - available only with `OPENED` or `CLICKED` activity paramater
#### Filtering You can filter results by using one or more of the following parameters: | Parameter | Required | Type | Description | | ------------- | -----| -------- | ---------------------------------- | | `id` | No | integer | Comma-separated list of prospect IDs to retrieve | | `status` | No | string | Prospect's **global** status: `ACTIVE`, `BOUNCED`, `REPLIED`, `BLACKLIST`, `INVALID` | | `contacted` | No | boolean | Whether a prospect has ever been contacted | | `interested` | No | string | Returns prospects only if they have a specific interest level set in any of the campaigns they are enrolled in. Available values: `INTERESTED`, `MAYBE-LATER`, `NOT-INTERESTED`, `NOT-MARKED`. A prospect marked INTERESTED in N campaigns will return N prospect objects. Please mind `_` in the response | | `activity` | No | string | Returns prospects based on their click and open activity. Available values: `OPENED`, `NOT-OPENED`, `CLICKED`, `NOT-CLICKED` | | `diff` | No | string | Return prospects whose activity timestamp is **greater than** the provided date. Uses ISO 8601-like format: `2025-01-15T00:00:00%2B0200`. Available timestamps: `updated`, `last_opened` (only with `OPENED` activity paramater), `last_clicked` (only with `CLICKED` activity paramater) | ### Request samples #### Get a list of prospects The example below showcases how to fetch last 50 most recently updated prospects with a global status `REPLIED`. ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v1/prospects?sort=-updated&status=REPLIED&per_page=50" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getRepliedProspects(): url = "https://api.woodpecker.co/rest/v1/prospects?sort=-updated&status=REPLIED&per_page=50" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getRepliedProspects() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getRepliedProspects(); } public static void getRepliedProspects() { try { String url = "https://api.woodpecker.co/rest/v1/prospects?sort=-updated&status=REPLIED&per_page=50"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getRepliedProspects() { const url = "https://api.woodpecker.co/rest/v1/prospects?sort=-updated&status=REPLIED&per_page=50"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getRepliedProspects(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('prospects', [ 'query' => [ 'sort' => '-updated', 'status' => 'REPLIED', 'per_page' => 50, ], ]); 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 headers | Header | Type | Description | | ------------- | -------- | --------------------------------- | | `X-Total-Count` | integer | Total number of prospects that match the criteria. Do not use together with `campaigns_details` parameter | ### Response examples An array of prospect objects. Some fields will be returned only when the request uses specific paramaters. Such properties are not available in the example below and have a note in the [body schema](#body-schema) ```json [ { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "organization_id": 654321789, "industry": "IT", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "state": "California", "country": "United States", "last_contacted": "2025-03-20T14:32:34+0100", "last replied": "2025-03-21T08:11:35+0100", "updated": "2025-03-21T08:11:35+0100", "encrypted": false, "snipet1": "You are running a successful startup incubator Bachmanity", "snipet2": "", "snipet3": "", "snipet4": "", "snippet1": "", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "custom_snippet": "You are running a successful startup incubator Bachmanity" }, "status": "REPLIED" } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `organization_id` | integer | Unique identifier of a prospect's company | |   └─ `industry` | string | Prospect's industry | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `state` | string | Prospect's state or region | |   └─ `country` | string | Prospect's country | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `last replied` | string | Date when the prospect last replied (ISO 8601 format). Note the missing `_` | |   └─ `updated` | string | Date when the prospect was last updated (ISO 8601 format) | |   └─ `encrypted` | boolean | Whether a prospect is [encrypted](https://woodpecker.co/help-center/en/articles/5258897) | |   └─ `snipet` | string | Legacy. Always equal to the corresponding `snippetX` values. There are 4 snipet fields (`snipet1` to `snipet4`) | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `status` | string | Prospect's **global** status | |   └─ `interested` | string/null | Deprecated. Available only with `interested` parameter and when an interest level is defined. Available values: `INTERESTED`, `MAYBE-LATER`, `NOT-INTERESTED`. Please mind `_` instead of `-` | |   └─ `interest_level` | object/null | Prospect's Interest Level information. Available only with `interested` parameter | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer/null | Available only with `interested` parameter. Campaign ID where the prospect has a defined interest level | |   └─ `sent_mails` | integer/null | Available only with `activity` parameter. Total number of emails sent in all campaigns | |   └─ `last_open` | string/null | Available only with `OPENED` activity parameter. Timestamp when the prospect has last opened an email | |   └─ `last_clicked` | string/null | Available only with `CLICKED` activity parameter. Timestamp when the prospect has last clicked a link in an email | |   └─ `click_url` | string/null | Available only with `CLICKED` activity parameter. URL of the clicked link. Each clicked link will result in a separate prospect object | There are no prospects matching your criteria. Please review the request parameters and your prospect database. ```json { "message": "There are no prospects matching the given criteria." } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `message` | string | Descriptive response message | Invalid request or malformed request syntax. ```json { "status": { "status": "ERROR", "code": "E_WRONG_PARAM", "msg": "Wrong param [param_name]=requested_param" | "Unknown param:madeUpParam" | "Wrong param, required activity=OPENED|NOT-OPENED" | "Wrong param, required activity=CLICKED|NOT-CLICKED" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Search for prospects :::warning This is a v1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to v2. While it remains functional, consider handling errors accordingly. ::: Search for prospects that match your criteria, including email, company, tags, and other available fields. Retrieve a paginated list of prospects in your account, each containing snippet data, global status, and key dates such as the last contacted date. The `campaigns_details` object provides information about the campaigns a prospect is enrolled in, including their campaign status, local status, and interest level. You can also [sort](#sorting-and-pagination) prospects or [filter](#filtering) them by specific fields and values.
Fields available for searching * email * first_name * last_name * company * organization_id * industry * website * tags * title * phone * address * city * state * country * snipet1 - snipet4 * snippet5 - snippet15
## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v1/prospects?campaigns_details=true&search={search-field}={search_value},{search-field-2}={search_value-2} ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters #### Searching Use the `search` parameter to find prospects matching your specific criteria | Parameter | Required | Type | Description | | ------------- | -----| -------- | ---------------------------------- | | `search` | Yes | string | Comma-separated list of search criteria. A full list of searchable fields is available below. for `email`, you can use a full email address format `recipientname@mail.com` for an exact match or a string `recipientname` to look for a regex match `.*recipientname.*` You can search through multiple fields, for example: `?search=email=drew,company=companyName` Multiple search fields use `AND` operator; multiple fields of the same type use `OR` Fields other than `email` and `organization_id` use broad search `.*{search-value}.*` The `tags` parameter is case-sensitive|
Fields available for searching * email * first_name * last_name * company * organization_id * industry * website * tags - case-sensitive search * title * phone * address * city * state * country * snipet1 - snipet4 * snippet5 - snippet15
#### Filtering You can filter results by using one or more of the following parameters: | Parameter | Required | Type | Description | | ------------- | -----| -------- | ---------------------------------- | | `id` | No | integer | Comma-separated list of prospect IDs to retrieve | | `campaigns_details` | No | boolean | Whether to add details of campaigns a prospect belongs to | | `status` | No | string | Prospect's **global** status: `ACTIVE`, `BOUNCED`, `REPLIED`, `BLACKLIST`, `INVALID` | | `campaigns_id` | No | integer | Comma-separated list of campaign IDs that prospects are enrolled in. Using this parameter will result in response body as described in [getting prospects in campaign](get-prospects-campaign.mdx) - refer to this guide | | `contacted` | No | boolean | Whether a prospect has ever been contacted | | `interested` | No | string | Returns prospects only if they have a specific interest level set in any of the campaigns they are enrolled in. Available values: `INTERESTED`, `MAYBE-LATER`, `NOT-INTERESTED`, `NOT-MARKED` | | `diff` | No | string | Filters prospects whose activity timestamp is later than the provided value. The format must be `diff=activity>timestamp`, where `activity` is one of: `updated`, `last_opened` (requires `activity=OPENED`), or `last_clicked` (requires `activity=CLICKED`). The `timestamp` must be in ISO 8601 format (`2025-01-15T00:00:00+0200`). URL encoding is required (`+` becomes `%2B`, `>` becomes `%3E`) | #### Sorting and pagination | Parameter | Required | Type | Description | | ------------- | -------- | ----- | -------------------- | | `page` | No | integer | Requested results page (1-based) | | `per_page` | No | integer | Number of records per page. Default: 100, maximum: 1000 | | `sort` | No | string | Order the results based on the specified column. Default: order by ID ascending | For sorting, use `+` before the field name for ascending order and `-` for descending order. To sort by multiple fields, separate them with a comma. To sort prospects by `company` name ascending and `last_contacted` date descending use: `sort=+company,-last_contacted`
Fields available for sorting * id * status * updated * email * last_contacted * last_replied * first_name * last_name * company * organization_id * industry * website * tags * title * phone * address * city * state * country * snipet1 - snipet4 * snippet5 - snippet15 * last_opened - available only with `OPENED` activity paramater * last_clicked - available only with `CLICKED` activity paramater * sent_mails - available only with `OPENED` or `CLICKED` activity paramater
### Request samples #### Search for prospects The example below showcases how to search for a prospect with a specific email, together with details about the campaigns they are enrolled to. ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v1/prospects?campaigns_details=true&search=email=recipientname@mail.com" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def searchProspectByEmail(): url = "https://api.woodpecker.co/rest/v1/prospects?campaigns_details=true&search=email=recipientname@mail.com" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": searchProspectByEmail() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { searchProspectByEmail(); } public static void searchProspectByEmail() { try { String url = "https://api.woodpecker.co/rest/v1/prospects?campaigns_details=true&search=email=recipientname@mail.com"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function searchProspectByEmail() { const url = "https://api.woodpecker.co/rest/v1/prospects?campaigns_details=true&search=email=recipientname@mail.com"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } searchProspectByEmail(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('prospects', [ 'query' => [ 'campaigns_details' => 'true', 'search' => 'email=recipientname@mail.com', ], ]); 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 An array of prospect objects. Some fields will be returned only when the request uses specific paramaters. Such properties are not available in the example below and have a note in the [body schema](#body-schema) ```json [ { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "organization_id": 654321789, "industry": "IT", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "state": "California", "country": "United States", "last_contacted": "2025-03-20T14:32:34+0100", "last replied": "2025-03-21T08:11:35+0100", "updated": "2025-03-21T08:11:35+0100", "encrypted": false, "snipet1": "You are running a successful startup incubator Bachmanity", "snipet2": "", "snipet3": "", "snipet4": "", "snippet1": "", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "custom_snippet": "You are running a successful startup incubator Bachmanity" }, "status": "REPLIED", "campaigns_details": [ { "campaign_id": 654321, "campaign_name": "SaaS in America", "campaign_status": "RUNNING", "campaign_prospect_status": "ACTIVE", "interested": "NOT_MARKED", "interest_level": { "level": "NOT_MARKED", "ai_detected": false } }, { "campaign_id": 987654, "campaign_name": "SaaS in Europe", "campaign_status": "RUNNING", "campaign_prospect_status": "REPLIED", "interested": "INTERESTED", "interest_level": { "level": "INTERESTED", "ai_detected": true } } ] } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `organization_id` | integer | Unique identifier of a prospect's company | |   └─ `industry` | string | Prospect's industry | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space. Searching by tags is case-sensitive | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `state` | string | Prospect's state or region | |   └─ `country` | string | Prospect's country | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `last replied` | string | Date when the prospect last replied (ISO 8601 format). Note the missing `_` | |   └─ `updated` | string | Date when the prospect was last updated (ISO 8601 format) | |   └─ `encrypted` | boolean | Whether a prospect is [encrypted](https://woodpecker.co/help-center/en/articles/5258897) | |   └─ `snipet` | string | Legacy. Always equal to the corresponding `snippetX` values. There are 4 snipet fields (`snipet1` to `snipet4`) | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `status` | string | Prospect's **global** status | |   └─ `campaigns_details` | array[object] | Campaign details | |     └─└─ `[].campaign_id` | integer | Campaign ID the prospect is enrolled in | |     └─└─ `[].campaign_name` | string | Campaign name | |     └─└─ `[].campaign_status` | string | Current status of the campaign. Available values: `RUNNING`, `DRAFT`, `STOPPED`, `PAUSED`, `EDITED`, `COMPLETED` | |     └─└─ `[].campaign_prospect_status` | string | Current campaign status of a prospect. Available values: `ACTIVE`, `BOUNCED`, `TO-CHECK`, `TO-REVIEW`, `REPLIED`, `AUTOREPLIED`, `BLACKLIST`, `PAUSED`, `INVALID` | |     └─└─ `[].interested` | string | Deprecated. Interest level of a prospect in a given campaign. Available values: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED`, `NOT_MARKED`. | |     └─└─ `[].interest_level` | object | Prospect's Interest Level information in a given campaign | |        └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |        └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |     └─└─ `[].sent_mails` | integer/null | Available only with `activity` parameter. Total number of emails sent in all campaigns | |     └─└─ `[].last_open` | string/null | Available only with `OPENED` activity parameter. Timestamp when the prospect has last opened an email | |     └─└─ `[].last_clicked` | string/null | Available only with `CLICKED` activity parameter. Timestamp when the prospect has last clicked a link in an email | |     └─└─ `[].click_url` | string/null | Available only with `CLICKED` activity parameter. URL of the clicked link. Each clicked link will result in a separate prospect object | There are no prospects matching your criteria. Please review the request parameters and your prospect database. ```json { "message": "There are no prospects matching the given criteria." } ``` #### Body schema | Field | Data Type | Description | |---------------|-----------------|--------------------------------------------------| | `message` | string | Descriptive response message | Invalid request or malformed request syntax. ```json { "status": { "status": "ERROR", "code": "E_WRONG_PARAM", "msg": "Wrong param [param_name]=requested_param" | "Unknown param:madeUpParam" | "Wrong param, required activity=OPENED|NOT-OPENED" | "Wrong param, required activity=CLICKED|NOT-CLICKED" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | API access denied. You subscription might not be active, lack the API add-on, or the key belongs to an inactive client company. ```json { "status": { "status": "ERROR", "code": "E_NO_PERMISSION", "msg": "Api access denied." | "You need to have an API keys addon to access our API." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [rate limits](/docs/getting-started/rate-limiting.md). API v1 is subject to the same rate limits as v2, however the response code is `409` instead of `429`. ```json { "status": { "status": "ERROR", "code": "E_TOO_MANY_REQUESTS", "msg": "Too many requests in one time" } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema A list of all available `status.code` values is available [here](prospects.mdx#error-codes) | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Prospects The `/prospects` endpoints allows you to manage the prospects stored in your database. You can fetch, add, and update prospects. Each prospect has a global status as well as a campaign-specific status. Since a prospect can be enrolled in multiple campaigns, their interest level is assigned per campaign. You can: * retrieve prospect responses [here](GET-prospect-responses.mdx) * retrieve prospects stored in your account [here](get-prospects.mdx) * retrieve prospects in campaigns [here](get-prospects-campaign.mdx) * search for prospects [here](get-search-prospects.mdx) * add or update prospects in global prospect list [here](POST-add-prospects-list.mdx) * add or update prospects in a campaign [here](POST-update-prospects-campaign.mdx) ### Error codes The `v1/prospects` endpoints use an older version of the API, and their error responses differ from those of other endpoints. Each v1 endpoint includes its own error descriptions, but for reference, the table below lists the error codes returned by the v1 API in `status.code` property. | Error Code | Error Description | |----------------------|------------------| | `E_DUPLICATE` | Duplicate. The prospect has been added to your prospect base before | | `E_EMAIL` | Invalid email format | | `E_EMAIL_NOT_EXISTS` | Looks like this email doesn't exist. | | `E_INV_STATUS` | Your prospect's status is other than ACTIVE. Refer to [force method](POST-add-prospects-campaign.mdx#body-schema)| | `E_RECORD_NOT_FOUND` | The resource cannot be found | | `E_REQUIRED_ELEMENT` | Missing `prospects` object | | `E_SESSION` | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) | | `E_TOO_MANY_REQUESTS` | Too many simultaneous requests. Refer to [rate limits](/docs/getting-started/rate-limiting.md) | | `E_URL_NOT_FOUND` | Invalid URL. Please review the request URL | | `E_WRONG_PARAM` | Invalid parameter / Invalid target_url | --- ## Complete statistics for each level of campaign This report provides an overview of campaign statistics, including the number of contacted prospects, response rate, bounce rate, interest levels, and more. It focuses on **step-level metrics**. For detailed statistics at campaign-level, please refer to the [General statistics per campaign](./General-statistics.mdx) report. - The statistics are grouped by campaign, campaign step, and step's A/B version. - Campaigns with YES/NO paths have the results aggregated per step, not per path. - Campaigns and campaign steps that did not send any messages during the selected period will not be included in the results. You can preview example results [below](#response-1) ## Generating a report Use the below endpoint to generate a report `hash`. Afterwards use the [rest/v2/reports/\{hash\}](#retrieving-a-report) to retrieve the statistics data. ### Request #### Endpoint ``` POST https://api.woodpecker.co/rest/v2/reports/complete_statistics_for_each_level_of_campaign ``` #### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). #### Body A JSON object containing `from` and `to` date fields, that define the date period for the report's data generation. :::info You can generate data for up to 30 last days ::: ```json { "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" } ``` | Field | Type | Description | Example | | ------ | ------ | ------------------------------------------- | -------------- | | `from` | string | Start date of the report in ISO 8601 format | `"2025-01-01"` | | `to` | string | End date of the report in ISO 8601 format | `"2025-01-31"` | #### Request samples ##### Generate a report hash ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/reports/complete_statistics_for_each_level_of_campaign" \ --header "Content-Type: application/json" \ --header "x-api-key: {YOUR_API_KEY}" \ --data '{ "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" }' ``` ```Python import requests def getCampaignStatistics(): url = "https://api.woodpecker.co/rest/v2/reports/complete_statistics_for_each_level_of_campaign" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "from": "2024-01-01", "to": "2024-12-31" } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": getCampaignStatistics() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getCampaignStatistics(); } public static void getCampaignStatistics() { try { String url = "https://api.woodpecker.co/rest/v2/reports/complete_statistics_for_each_level_of_campaign"; String jsonData = "{" + "\"from\": \"2024-01-01\"," + "\"to\": \"2024-12-31\"" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getCampaignStatistics() { const url = "https://api.woodpecker.co/rest/v2/reports/complete_statistics_for_each_level_of_campaign"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { from: "2024-01-01", to: "2024-12-31" }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getCampaignStatistics(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('reports/complete_statistics_for_each_level_of_campaign', [ 'json' => [ 'from' => 'YYYY-MM-DD', 'to' => 'YYYY-MM-DD', ], ]); 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 The response is a hash that you should use the fetch the report content using the [rest/v2/reports/\{hash\}](#retrieving-a-report) endpoint. ```json { "hash":"c966572e5b7c12d73f....347b5186242782c9550d" } ``` ##### Body schema | Field | Type | Description| |-------|----------|--------------------------| | `hash` | string | Representation of the report's ID. Use it to fetch the generated data | Invalid request parameters or malformed request syntax. Please review the [request body](#body) ```json { "title": "Bad Request", "status": 400, "detail": "Reports can be generated from the last 30 days. Change the from parameter" | "Value of to is incorrect." | "Value of from is incorrect." | "From date cannot be later than the to date.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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](#endpoint) ```json { "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 | Incorrect report name in the URL. Please review the [endpoint URL](#endpoint) ``` Status: 405 Body: None ``` Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ## Retrieving a report After requesting the report generation, you can fetch it using the below endpoint. Insert the hash, obtained from the above request, to the URL. Preparing the data may take some time. Please check the `status` value to monitor the progress. The possible statuses are `PENDING`, `WAITING`, `IN_PROGRESS`, `READY` and `FAILED`. ### Request #### Endpoint ``` GET https://api.woodpecker.co/rest/v2/reports/{hash} ``` #### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). #### Request samples ##### Retrieve a report using the hash ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/reports/{hash}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getReportByHash(report_hash): url = f"https://api.woodpecker.co/rest/v2/reports/{report_hash}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getReportByHash("abc123def456ghi789") # Example hash ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { String reportHash = "abc123def456ghi789"; // Example hash getReportByHash(reportHash); } public static void getReportByHash(String hash) { try { String url = "https://api.woodpecker.co/rest/v2/reports/" + hash; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getReportByHash(reportHash) { const url = `https://api.woodpecker.co/rest/v2/reports/${reportHash}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getReportByHash("abc123def456ghi789"); // Example hash ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $reportHash = '{hash}'; try { $response = $client->get("reports/{$reportHash}"); 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 This example presents campaign statistics for the period from January 1 to January 31, 2025, covering two campaigns. You can observe ten objects as both campaigns utilize three A/B/C versions of the first step and one version for steps two and three.
Successful response example ```json { "status": "READY", "report": { "description": "Complete_statistics_for_each_level_of_campaign_2025-01-01-2025-01-31", "data": [ { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 1234567, "name": "First successful campaign", "campaign_status": "RUNNING", "step": 1, "version": "A", "sent": 75, "bounced": 1, "bounce_rate": 1.3, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 74, "responded": 4, "responded_rate": 5.4, "interested_yes": 2, "interested_maybe": 0, "interested_no": 2 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 1234567, "name": "First successful campaign", "campaign_status": "RUNNING", "step": 1, "version": "B", "sent": 76, "bounced": 1, "bounce_rate": 1.3, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 75, "responded": 4, "responded_rate": 5.3, "interested_yes": 1, "interested_maybe": 0, "interested_no": 3 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 1234567, "name": "First successful campaign", "campaign_status": "RUNNING", "step": 1, "version": "C", "sent": 78, "bounced": 2, "bounce_rate": 2.6, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 76, "responded": 3, "responded_rate": 3.9, "interested_yes": 1, "interested_maybe": 0, "interested_no": 2 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 1234567, "name": "First successful campaign", "campaign_status": "RUNNING", "step": 2, "version": "A", "sent": 201, "bounced": 1, "bounce_rate": 0.5, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 200, "responded": 10, "responded_rate": 5.0, "interested_yes": 0, "interested_maybe": 3, "interested_no": 5 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 1234567, "name": "First successful campaign", "campaign_status": "RUNNING", "step": 3, "version": "A", "sent": 177, "bounced": 0, "bounce_rate": 0.0, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 177, "responded": 3, "responded_rate": 1.7, "interested_yes": 0, "interested_maybe": 0, "interested_no": 3 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 9876543, "name": "Second campaign", "campaign_status": "RUNNING", "step": 1, "version": "A", "sent": 34, "bounced": 1, "bounce_rate": 2.9, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 33, "responded": 0, "responded_rate": 0.0, "interested_yes": 0, "interested_maybe": 0, "interested_no": 0 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 9876543, "name": "Second campaign", "campaign_status": "RUNNING", "step": 1, "version": "B", "sent": 32, "bounced": 1, "bounce_rate": 3.1, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 31, "responded": 3, "responded_rate": 9.7, "interested_yes": 0, "interested_maybe": 1, "interested_no": 2 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 9876543, "name": "Second campaign", "campaign_status": "RUNNING", "step": 1, "version": "C", "sent": 31, "bounced": 0, "bounce_rate": 0.0, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 31, "responded": 1, "responded_rate": 3.2, "interested_yes": 0, "interested_maybe": 0, "interested_no": 1 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 9876543, "name": "Second campaign", "campaign_status": "RUNNING", "step": 2, "version": "A", "sent": 89, "bounced": 0, "bounce_rate": 0.0, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 89, "responded": 4, "responded_rate": 4.5, "interested_yes": 0, "interested_maybe": 1, "interested_no": 3 }, { "client": "Bachmanity", "campaign_owner": "Erlich", "id": 9876543, "name": "Second campaign", "campaign_status": "RUNNING", "step": 3, "version": "A", "sent": 69, "bounced": 0, "bounce_rate": 0.0, "opened": 0, "opened_rate": 0.0, "clicked": 0, "opt_out": 0, "opt_out_rate": 0.0, "delivered": 69, "responded": 1, "responded_rate": 1.4, "interested_yes": 0, "interested_maybe": 0, "interested_no": 1 } ] } } ```
##### Body schema | Field | Type | Description | |-------------------------------|----------|-----------------------------------------| | `status` | string | Status of the generation. `PENDING`, `WAITING`, `IN_PROGRESS`, `READY`, `FAILED`| | `report` | object/null | Container for report details. Null if `status` is not `READY` | | └─`report.description` | string | Full name of the report and its period | | └─`report.data` | array | List of campaign statistics | |     └─`data[].client` | string | Name of the Woodpecker account| |     └─`data[].campaign_owner` | string | Full name of the Woodpecker user who created the campaign | |     └─`data[].id` | integer | Unique ID of the campaign | |     └─`data[].name` | string | Name of the campaign | |     └─`data[].campaign_status` | string | Current status of the campaign. One of: `RUNNING` `PAUSED` `STOPPED` `EDITED` `DRAFT` `COMPLETED` `DELETED` | |     └─`data[].step` | integer | Step of the campaign | |     └─`data[].version` | string | A/B/C/D/E version of of a step | |     └─`data[].sent` | integer | Number of contacted prospects | |     └─`data[].bounced` | integer | Number of prospects who bounced | |     └─`data[].bounce_rate` | string | Percentage of prospects who bounced | |     └─`data[].opened` | integer | Number of prospects who opened an email | |     └─`data[].opened_rate` | string | Percentage of prospects who opened an email | |     └─`data[].clicked` | integer | Number of prospects who who clicked a link | |     └─`data[].opt_out` | integer | Number of prospects who opted-out | |     └─`data[].opt_out_rate` | integer | Percentage of prospects who opted-out | |     └─`data[].delivered` | integer | Number of prospects who received an email | |     └─`data[].responded` | integer | Number of prospects who responded | |     └─`data[].responded_rate` | string | Percentage of prospects who responded | |     └─`data[].interested_yes` | integer | Number of "interested" responses | |     └─`data[].interested_maybe` | integer | Number of "maybe later" responses | |     └─`data[].interested_no` | integer | Number of "not interested" responses | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 | Report not found - please check the hash or the request URL. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## General statistics per campaign This report provides an overview of campaign statistics, including the number of contacted prospects, response rate, bounce rate, interest levels, and more. It focuses on **campaign-level metrics**. For detailed statistics at each step of the campaign, please refer to the [Complete statistics for each level of campaign](./Complete-statistics.mdx) report. - All of the campaign metrics are counted as **distinct events per prospect, in a given period** - if one prospect receives an opening email and a followup in one campaign, in the defined period, the `delivered` statistic will be counted as 1, as one prospect has been delivered an email. - The statistics are grouped by campaign and sending email. This means that you might see statistics for the same campaign multiple times if it is sent from multiple mailboxes. - Campaigns that did not send any messages during the selected period will not be included in the results. You can preview example results [below](#response-1) ## Generating a report Use the below endpoint to generate a report `hash`. Afterwards use the [rest/v2/reports/\{hash\}](#retrieving-a-report) to retrieve the statistics data. ### Request #### Endpoint ``` POST https://api.woodpecker.co/rest/v2/reports/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](/docs/getting-started/authentication.mdx). #### Body A JSON object containing `from` and `to` date fields, that define the date period for the report's data generation. :::info You can generate data for up to 30 last days ::: ```json { "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" } ``` | Field | Type | Description | Example | | ------ | ------ | ------------------------------------------- | -------------- | | `from` | string | Start date of the report in ISO 8601 format | `"2025-01-01"` | | `to` | string | End date of the report in ISO 8601 format | `"2025-01-31"` | #### Request samples ##### Generate a report hash ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/reports/campaigns" \ --header "Content-Type: application/json" \ --header "x-api-key: {YOUR_API_KEY}" \ --data '{ "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" }' ``` ```Python import requests def getCampaignReport(): url = "https://api.woodpecker.co/rest/v2/reports/campaigns" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "from": "2024-01-01", "to": "2024-12-31" } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": getCampaignReport() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getCampaignReport(); } public static void getCampaignReport() { try { String url = "https://api.woodpecker.co/rest/v2/reports/campaigns"; String jsonData = "{" + "\"from\": \"2024-01-01\"," + "\"to\": \"2024-12-31\"" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getCampaignReport() { const url = "https://api.woodpecker.co/rest/v2/reports/campaigns"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { from: "2024-01-01", to: "2024-12-31" }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getCampaignReport(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('reports/campaigns', [ 'json' => [ 'from' => 'YYYY-MM-DD', 'to' => 'YYYY-MM-DD', ], ]); 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 The response is a hash that you should use the fetch the report content using the [rest/v2/reports/\{hash\}](#retrieving-a-report) endpoint. ```json { "hash":"c966572e5b7c12d73f....347b5186242782c9550d" } ``` ##### Body schema | Field | Type | Description| |-------|----------|--------------------------| | `hash` | string | Representation of the report's ID. Use it to fetch the generated data | Invalid request parameters or malformed request syntax. Please review the [request body](#body) ```json { "title": "Bad Request", "status": 400, "detail": "Reports can be generated from the last 30 days. Change the from parameter" | "Value of to is incorrect." | "Value of from is incorrect." | "From date cannot be later than the to date.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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](#endpoint) ```json { "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 | Incorrect report name in the URL. Please review the [endpoint URL](#endpoint) ``` Status: 405 Body: None ``` Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ## Retrieving a report After requesting the report generation, you can fetch it using the below endpoint. Insert the hash, obtained from the above request, to the URL. Preparing the data may take some time. Please check the `status` value to monitor the progress. The possible statuses are `PENDING`, `WAITING`, `IN_PROGRESS`, `READY` and `FAILED`. ### Request #### Endpoint ``` GET https://api.woodpecker.co/rest/v2/reports/{hash} ``` #### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). #### Parameters By default, the results are sorted by `data[].id` ascending. You can change the order by using the `sort` parameter. | Key | Value | Required | Description | |-------------|----------|--------|---------------------------------------------------| | `sort` | `+id`/`-id` | No | Sort the results by `data[].id`. Use `-` for descending and `+` for ascending | #### Request samples ##### Retrieve a report using the hash ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/reports/{hash}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getReportByHash(report_hash): url = f"https://api.woodpecker.co/rest/v2/reports/{report_hash}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getReportByHash("abc123def456ghi789") # Example hash ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { String reportHash = "abc123def456ghi789"; // Example hash getReportByHash(reportHash); } public static void getReportByHash(String hash) { try { String url = "https://api.woodpecker.co/rest/v2/reports/" + hash; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getReportByHash(reportHash) { const url = `https://api.woodpecker.co/rest/v2/reports/${reportHash}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getReportByHash("abc123def456ghi789"); // Example hash ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); $hash = '{hash}'; try { $response = $client->get("reports/{$hash}"); 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 This example presents campaign statistics for the period from January 1 to January 31, 2025, covering two campaigns sent from two email addresses. You can observe three objects as the "First Successful Campaign" utilizes inbox rotation and includes statistics for both senders, while "Second campaign" uses one sending address. ```json { "status": "READY", "report": { "description": "General_statistics_per_campaign_2025-01-01-2025-01-31", "data": [ { "id": 1234567, "name": "First successful campaign", "status": "RUNNING", "sent_from": "email-1@gmail.com", "contacted_prospects": 64, "bounced": 0, "bounced_rate": "0.0%", "opened": 0, "open_rate": "0.0%", "clicked": 0, "opt_out": 0, "delivered": 64, "responded": 7, "response_rate": "10.9%", "interested": 2, "maybe_later": 1, "not_interested": 4 }, { "id": 1234567, "name": "First successful campaign", "status": "RUNNING", "sent_from": "email-2@gmail.com", "contacted_prospects": 67, "bounced": 0, "bounced_rate": "0.0%", "opened": 0, "open_rate": "0.0%", "clicked": 0, "opt_out": 0, "delivered": 67, "responded": 8, "response_rate": "11.9%", "interested": 0, "maybe_later": 0, "not_interested": 6 }, { "id": 9876543, "name": "Second campaign", "status": "RUNNING", "sent_from": "email-1@gmail.com", "contacted_prospects": 56, "bounced": 0, "bounced_rate": "0.0%", "opened": 0, "open_rate": "0.0%", "clicked": 0, "opt_out": 0, "delivered": 56, "responded": 8, "response_rate": "14.3%", "interested": 3, "maybe_later": 0, "not_interested": 4 } ] } } ``` ##### Body schema | Field | Type | Description | |-------------------------------|----------|-----------------------------------------| | `status` | string | Status of the generation. `PENDING`, `WAITING`, `IN_PROGRESS`, `READY`,`FAILED` | | `report` | object/null | Container for report details. Null if `status` is not `READY` | | └─`report.description` | string | Full name of the report and its period | | └─`report.data` | array | List of campaign statistics | |     └─`data[].id` | integer | Unique ID of the campaign | |     └─`data[].name` | string | Name of the campaign | |     └─`data[].status` | string | Current status of the campaign. One of: `RUNNING` `PAUSED` `STOPPED` `EDITED` `DRAFT` `COMPLETED` `DELETED` | |     └─`data[].sent_from` | string | Email address used to send the campaign | |     └─`data[].contacted_prospects` | integer | Number of contacted prospects | |     └─`data[].bounced` | integer | Number of prospects who bounced | |     └─`data[].bounced_rate` | string | Percentage of prospects who bounced | |     └─`data[].opened` | integer | Number of prospects who opened an email | |     └─`data[].open_rate` | string | Percentage of prospects who opened an email | |     └─`data[].clicked` | integer | Number of prospects who who clicked a link | |     └─`data[].opt_out` | integer | Number of prospects who opted-out | |     └─`data[].delivered` | integer | Number of prospects who received an email | |     └─`data[].responded` | integer | Number of prospects who responded | |     └─`data[].response_rate` | string | Percentage of prospects who responded | |     └─`data[].interested` | integer | Number of "interested" responses | |     └─`data[].maybe_later` | integer | Number of "maybe later" responses | |     └─`data[].not_interested` | integer | Number of "not interested" responses | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 | Report not found - please check the hash or the request URL. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Open rate per campaign message sent on this date has this many opens. This report focuses on the open rate of the emails sent from your campaigns. It breaks down the number of emails sent and their open rates by campaign, day, mailbox, path, step, and version. For campaigns sent from multiple mailboxes, the data will be separated by each mailbox. You can preview example results [below](#response-1) ## Generating a report Use the below endpoint to generate a report `hash`. Afterwards use the [rest/v2/reports/\{hash\}](#retrieving-a-report) to retrieve the statistics data. ### Request #### Endpoint ``` POST https://api.woodpecker.co/rest/v2/reports/open_rate ``` #### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). #### Body A JSON object containing `from` and `to` date fields, that define the date period for the report's data generation. :::info You can generate data for up to 30 last days ::: ```json { "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" } ``` | Field | Type | Description | Example | | ------ | ------ | ------------------------------------------- | -------------- | | `from` | string | Start date of the report in ISO 8601 format | `"2025-01-01"` | | `to` | string | End date of the report in ISO 8601 format | `"2025-01-31"` | #### Request samples ##### Generate a report hash ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/reports/open_rate" \ --header "Content-Type: application/json" \ --header "x-api-key: {YOUR_API_KEY}" \ --data '{ "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" }' ``` ```Python import requests def getOpenRateReport(): url = "https://api.woodpecker.co/rest/v2/reports/open_rate" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "from": "2024-01-01", "to": "2024-12-31" } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": getOpenRateReport() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getOpenRateReport(); } public static void getOpenRateReport() { try { String url = "https://api.woodpecker.co/rest/v2/reports/open_rate"; String jsonData = "{" + "\"from\": \"2024-01-01\"," + "\"to\": \"2024-12-31\"" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getOpenRateReport() { const url = "https://api.woodpecker.co/rest/v2/reports/open_rate"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { from: "2024-01-01", to: "2024-12-31" }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getOpenRateReport(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('reports/open_rate', [ 'json' => [ 'from' => 'YYYY-MM-DD', 'to' => 'YYYY-MM-DD', ], ]); 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 The response is a hash that you should use the fetch the report content using the [rest/v2/reports/\{hash\}](#retrieving-a-report) endpoint. ```json { "hash":"c966572e5b7c12d73f....347b5186242782c9550d" } ``` ##### Body schema | Field | Type | Description| |-------|----------|--------------------------| | `hash` | string | Representation of the report's ID. Use it to fetch the generated data | Invalid request parameters or malformed request syntax. Please review the [request body](#body) ```json { "title": "Bad Request", "status": 400, "detail": "Reports can be generated from the last 30 days. Change the from parameter" | "Value of to is incorrect." | "Value of from is incorrect." | "From date cannot be later than the to date.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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](#endpoint) ```json { "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 | Incorrect report name in the URL. Please review the [endpoint URL](#endpoint) ``` Status: 405 Body: None ``` Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ## Retrieving a report After requesting the report generation, you can fetch it using the below endpoint. Insert the hash, obtained from the above request, to the URL. Preparing the data may take some time. Please check the `status` value to monitor the progress. The possible statuses are `PENDING`, `WAITING`, `IN_PROGRESS`, `READY` and `FAILED`. ### Request #### Endpoint ``` GET https://api.woodpecker.co/rest/v2/reports/{hash} ``` #### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). #### Parameters By default, the results are sorted by `id`, `sent_date`, `step` and `version` ascending. You can change the order by using one of the `sort` parameter and **one of the values**. | Key | Value | Required | Description | |---------|-----------------------------------|------|----| | `sort` | `+id`/`-id` | No | Sort the results by `data[].id`. Use `-` for descending and `+` for ascending | | `sort` | `+sent_date`/`-sent_date`    | No | Sort the results by `data[].sent_date`. Use `-` for descending and `+` for ascending | #### Request samples ##### Retrieve a report using the hash ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/reports/{hash}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getReportByHash(report_hash): url = f"https://api.woodpecker.co/rest/v2/reports/{report_hash}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getReportByHash("abc123def456ghi789") # Example hash ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { String reportHash = "abc123def456ghi789"; // Example hash getReportByHash(reportHash); } public static void getReportByHash(String hash) { try { String url = "https://api.woodpecker.co/rest/v2/reports/" + hash; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getReportByHash(reportHash) { const url = `https://api.woodpecker.co/rest/v2/reports/${reportHash}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getReportByHash("abc123def456ghi789"); // Example hash ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('reports/campaigns', [ 'json' => [ 'from' => 'YYYY-MM-DD', 'to' => 'YYYY-MM-DD', ], ]); 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 This example presents campaign statistics for the period from January 10 to January 15, 2025, covering two campaigns. Each object represents a number of emails sent and their open rates, grouped by campaign, day, path, step and version. The campaign with ID 1234567 is a linear campaign that does not have any conditions/paths and doesn't include A/B versions either. In contrast, campaign 9876543 has a condition after the first email step, contains A/B versions of the first step email and is sent from two mailboxes. ```json { "status": "READY", "report": { "description": "Open_rate_per_campaign_2025-01-10-2025-01-15", "data": [ { "id": 1234567, "name": "Simple linear campaign", "sent_date": "2025-01-10", "sent_from": "email-0@mail.com", "path": "", "step": 1, "version": "A", "sent": 50, "delivered": 49, "opened": 22, "open_rate": "44.9%" }, { "id": 1234567, "name": "Simple linear campaign", "sent_date": "2025-01-14", "sent_from": "email-0@mail.com", "path": "", "step": 2, "version": "A", "sent": 49, "delivered": 49, "opened": 20, "open_rate": "40.8%" }, { "id": 9876543, "name": "Multiple mailboxes, condition and A/B versions", "sent_date": "2025-01-12", "sent_from": "email-1@mail.com", "path": "Path YES/NO", "step": 1, "version": "A", "sent": 25, "delivered": 25, "opened": 16, "open_rate": "64.0%" }, { "id": 9876543, "name": "Multiple mailboxes, condition and A/B versions", "sent_date": "2025-01-12", "sent_from": "email-1@mail.com", "path": "Path YES/NO", "step": 1, "version": "B", "sent": 25, "delivered": 25, "opened": 17, "open_rate": "68.0%" }, { "id": 9876543, "name": "Multiple mailboxes, condition and A/B versions", "sent_date": "2025-01-12", "sent_from": "email-2@mail.com", "path": "Path YES/NO", "step": 1, "version": "A", "sent": 30, "delivered": 29, "opened": 13, "open_rate": "44.8%" }, { "id": 9876543, "name": "Multiple mailboxes, condition and A/B versions", "sent_date": "2025-01-12", "sent_from": "email-2@mail.com", "path": "Path YES/NO", "step": 1, "version": "B", "sent": 30, "delivered": 28, "opened": 16, "open_rate": "57.1%" }, { "id": 9876543, "name": "Multiple mailboxes, condition and A/B versions", "sent_date": "2025-01-15", "sent_from": "email-1@mail.com", "path": "Path YES", "step": 2, "version": "A", "sent": 20, "delivered": 20, "opened": 10, "open_rate": "50.0%" }, { "id": 9876543, "name": "Multiple mailboxes, condition and A/B versions", "sent_date": "2025-01-15", "sent_from": "email-1@mail.com", "path": "Path NO", "step": 2, "version": "A", "sent": 20, "delivered": 20, "opened": 9, "open_rate": "45.0%" }, { "id": 9876543, "name": "Multiple mailboxes, condition and A/B versions", "sent_date": "2025-01-15", "sent_from": "email-2@mail.com", "path": "Path YES", "step": 2, "version": "A", "sent": 20, "delivered": 20, "opened": 11, "open_rate": "55.0%" }, { "id": 9876543, "name": "Multiple mailboxes, condition and A/B versions", "sent_date": "2025-01-15", "sent_from": "email-2@mail.com", "path": "Path NO", "step": 2, "version": "A", "sent": 20, "delivered": 19, "opened": 9, "open_rate": "47.3%" } ] } } ``` ##### Body schema | Field | Type | Description | |-------------------------------|----------|-----------------------------------------| | `status` | string | Status of the generation. `PENDING`, `WAITING`, `IN_PROGRESS`, `READY`, `FAILED`| | `report` | object/null | Container for report details. Null if `status` is not `READY` | | └─`report.description` | string | Full name of the report and its period | | └─`report.data` | array | List of campaign statistics | |     └─`data[].id` | integer | Unique ID of the campaign | |     └─`data[].name` | string | Name of the campaign | |     └─`data[].sent_date` | string | Date of sending emails from a specific step `YYYY-MM-DD` | |     └─`data[].path` | string | Indication from which path the emails were sent. `""` - there's no condition in this campaign; `PATH YES` - the sum of sent emails refers to the YES path on a given step; `PATH NO` - the sum of sent emails refers to the NO path on a given step; `PATH YES/NO` IF-condition is set up in a further step of the campaign, it has not been evaluated yet | |     └─`data[].step` | integer | Step of the campaign | |     └─`data[].version` | string | A/B/C/D/E version of of a step. Defaults to `A` | |     └─`data[].sent` | integer | Number of emails sent | |     └─`data[].delivered` | integer | Number of delivered emails | |     └─`data[].opened` | integer | Number of opened emails | |     └─`data[].open_rate` | integer | Percentage of emails opened / emails delivered | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 | Report not found - please check the hash or the request URL. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Number of messages sent from each level in campaigns This report focuses on the number of emails sent from your campaigns. It provides a breakdown of emails sent per campaign, day, path, step, and version. For campaigns sent from multiple mailboxes, the data will be presented as a total rather than by individual mailbox. To see a similar per-mailbox breakdown, review the [Open rate per campaign report](Open-rate.mdx). You can preview example results [below](#response-1) ## Generating a report Use the below endpoint to generate a report `hash`. Afterwards use the [rest/v2/reports/\{hash\}](#retrieving-a-report) to retrieve the statistics data. ### Request #### Endpoint ``` POST https://api.woodpecker.co/rest/v2/reports/messages ``` #### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). #### Body A JSON object containing `from` and `to` date fields, that define the date period for the report's data generation. :::info You can generate data for up to 30 last days ::: ```json { "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" } ``` | Field | Type | Description | Example | | ------ | ------ | ------------------------------------------- | -------------- | | `from` | string | Start date of the report in ISO 8601 format | `"2025-01-01"` | | `to` | string | End date of the report in ISO 8601 format | `"2025-01-31"` | #### Request samples ##### Generate a report hash ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/reports/messages" \ --header "Content-Type: application/json" \ --header "x-api-key: {YOUR_API_KEY}" \ --data '{ "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" }' ``` ```Python import requests def getMessageReport(): url = "https://api.woodpecker.co/rest/v2/reports/messages" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "from": "2024-01-01", "to": "2024-12-31" } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: print("POST successful:", response.json()) else: print("POST failed with status:", response.status_code) if __name__ == "__main__": getMessageReport() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getMessageReport(); } public static void getMessageReport() { try { String url = "https://api.woodpecker.co/rest/v2/reports/messages"; String jsonData = "{" + "\"from\": \"2024-01-01\"," + "\"to\": \"2024-12-31\"" + "}"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getMessageReport() { const url = "https://api.woodpecker.co/rest/v2/reports/messages"; const headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" }; const data = { from: "2024-01-01", to: "2024-12-31" }; try { const response = await axios.post(url, data, { headers: headers }); if (response.status === 200) { console.log("POST successful:", response.data); } else { console.error("POST failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getMessageReport(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('reports/messages', [ 'json' => [ 'from' => 'YYYY-MM-DD', 'to' => 'YYYY-MM-DD', ], ]); 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 The response is a hash that you should use the fetch the report content using the [rest/v2/reports/\{hash\}](#retrieving-a-report) endpoint. ```json { "hash":"c966572e5b7c12d73f....347b5186242782c9550d" } ``` ##### Body schema | Field | Type | Description| |-------|----------|--------------------------| | `hash` | string | Representation of the report's ID. Use it to fetch the generated data | Invalid request parameters or malformed request syntax. Please review the [request body](#body) ```json { "title": "Bad Request", "status": 400, "detail": "Reports can be generated from the last 30 days. Change the from parameter" | "Value of to is incorrect." | "Value of from is incorrect." | "From date cannot be later than the to date.", "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 | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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](#endpoint) ```json { "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 | Incorrect report name in the URL. Please review the [endpoint URL](#endpoint) ``` Status: 405 Body: None ``` Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ## Retrieving a report After requesting the report generation, you can fetch it using the below endpoint. Insert the hash, obtained from the above request, to the URL. Preparing the data may take some time. Please check the `status` value to monitor the progress. The possible statuses are `PENDING`, `WAITING`, `IN_PROGRESS`, `READY` and `FAILED`. ### Request #### Endpoint ``` GET https://api.woodpecker.co/rest/v2/reports/{hash} ``` #### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). #### Parameters By default, the results are sorted by `id`, `sent_date`, `step` and `version` ascending. You can change the order by using one of the `sort` parameter and **one of the values**. | Key | Value | Required | Description | |---------|-----------------------------------|----|----| | `sort` | `+id`/`-id` | No | Sort the results by `data[].id`. Use `-` for descending and `+` for ascending | | `sort` | `+sent_date`/`-sent_date`    | No | Sort the results by `data[].sent_date`. Use `-` for descending and `+` for ascending | #### Request samples ##### Retrieve a report using the hash ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/reports/{hash}" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getReportByHash(report_hash): url = f"https://api.woodpecker.co/rest/v2/reports/{report_hash}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getReportByHash("abc123def456ghi789") # Example hash ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { String reportHash = "abc123def456ghi789"; // Example hash getReportByHash(reportHash); } public static void getReportByHash(String hash) { try { String url = "https://api.woodpecker.co/rest/v2/reports/" + hash; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getReportByHash(reportHash) { const url = `https://api.woodpecker.co/rest/v2/reports/${reportHash}`; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getReportByHash("abc123def456ghi789"); // Example hash ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('reports/campaigns', [ 'json' => [ 'from' => 'YYYY-MM-DD', 'to' => 'YYYY-MM-DD', ], ]); 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 This example presents campaign statistics for the period from January 1 to January 13, 2025, covering two campaigns. Each object represents a number of emails sent, grouped by campaign, day, path, step and version. The campaign with ID 1234567 is a linear campaign that does not have any conditions/paths, but it does include multiple A/B versions for each step. In contrast, campaign 9876543 has a condition before the first email step, resulting in two separate objects for the emails sent from the second step, as they follow different paths. ```json { "status": "READY", "report": { "description": "Number_of_messages_sent_from_each_level_in_campaigns_2025-01-01-2025-01-13", "data": [ { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-01", "path": "", "step": 1, "version": "A", "sent": 25 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-01", "path": "", "step": 1, "version": "B", "sent": 25 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-01", "path": "", "step": 1, "version": "C", "sent": 25 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-01", "path": "", "step": 1, "version": "D", "sent": 25 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-03", "path": "", "step": 1, "version": "A", "sent": 10 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-03", "path": "", "step": 1, "version": "B", "sent": 10 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-03", "path": "", "step": 2, "version": "A", "sent": 25 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-03", "path": "", "step": 2, "version": "B", "sent": 25 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-03", "path": "", "step": 2, "version": "C", "sent": 25 }, { "id": 1234567, "name": "Campaign with A/B versions and no IF condition", "sent_date": "2025-01-03", "path": "", "step": 2, "version": "D", "sent": 25 }, { "id": 9876543, "name": "Campaign with A/B versions and a condition before the first email", "sent_date": "2025-01-08", "path": "Path YES/NO", "step": 1, "version": "A", "sent": 15 }, { "id": 9876543, "name": "Campaign with A/B versions and a condition before the first email", "sent_date": "2025-01-08", "path": "Path YES/NO", "step": 1, "version": "B", "sent": 15 }, { "id": 9876543, "name": "Campaign with A/B versions and a condition before the first email", "sent_date": "2025-01-08", "path": "Path YES/NO", "step": 1, "version": "C", "sent": 15 }, { "id": 9876543, "name": "Campaign with A/B versions and a condition before the first email", "sent_date": "2025-01-13", "path": "Path NO", "step": 2, "version": "A", "sent": 30 }, { "id": 9876543, "name": "Campaign with A/B versions and a condition before the first email", "sent_date": "2025-01-13", "path": "Path YES", "step": 2, "version": "A", "sent": 15 } ] } } ``` ##### Body schema | Field | Type | Description | |-------------------------------|----------|-----------------------------------------| | `status` | string | Status of the generation. `PENDING`, `WAITING`, `IN_PROGRESS`, `READY`, `FAILED`| | `report` | object/null | Container for report details. Null if `status` is not `READY` | | └─`report.description` | string | Full name of the report and its period | | └─`report.data` | array | List of campaign statistics | |     └─`data[].id` | integer | Unique ID of the campaign | |     └─`data[].name` | string | Name of the campaign | |     └─`data[].sent_date` | string | Date of sending emails from a specific step `YYYY-MM-DD` | |     └─`data[].path` | string | Indication from which path the emails were sent. `""` - there's no condition in this campaign; `PATH YES` - the sum of sent emails refers to the YES path on a given step; `PATH NO` - the sum of sent emails refers to the NO path on a given step; `PATH YES/NO` IF-condition is set up in a further step of the campaign, it has not been evaluated yet | |     └─`data[].step` | integer | Step of the campaign | |     └─`data[].version` | string | A/B/C/D/E version of of a step. Defaults to `A` | |     └─`data[].sent` | integer | Number of emails sent | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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 | Report not found - please check the hash or the request URL. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Reports To get an in-depth look into your campaigns' performance, you can generate one of the predefined reports. * [Complete statistics for each level of campaign](Complete-statistics.mdx) * [General statistics per campaign](General-statistics.mdx) * [Number of messages sent from each level in campaigns](Sent-messages.mdx) * [Open rate per campaign](Open-rate.mdx) :::tip To preview the report data containing your campaign results, you can generate a report in your account's [Deliverability -> Reports tab](https://app.woodpecker.co/panel?#deliverability/reports?period=Today). ::: ## Flow of generating reports: 1. Request report generation via `POST /rest/v2/reports/{report-name}`. Define the report type and period, store the report hash identifier. 2. Give the report a second to generate and use the hash to fetch results using the `GET rest/v2/reports/{hash}` endpoint --- ## Get a list of users Retrieve a list of active users in your account. Only confirmed and active accounts will be included. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/users ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters By default, the results are sorted by `content[].id` ascending. You can change the order by using the `sort` parameter. | Key | Value | Required | Description | |--------|-------------|----------|----------------------------------------------------------------------------------| | `sort` | `+id`/`-id` | No | Sort the results by `content[].id`. Use `-` for descending and `+` for ascending | | `page` | integer | No | Requested results page (0-based) | ### Request samples #### Retrieve newest users ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/users?page=0&sort=-id" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def getUsers(): url = "https://api.woodpecker.co/rest/v2/users?page=0&sort=-id" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: print("GET successful:", response.json()) else: print("GET failed with status:", response.status_code) if __name__ == "__main__": getUsers() ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { getUsers(); } public static void getUsers() { try { String url = "https://api.woodpecker.co/rest/v2/users?page=0&sort=-id"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(url)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require("axios"); async function getUsers() { const url = "https://api.woodpecker.co/rest/v2/users?page=0&sort=-id"; const headers = { "x-api-key": "{YOUR_API_KEY}" }; try { const response = await axios.get(url, { headers: headers }); if (response.status === 200) { console.log("GET successful:", response.data); } else { console.error("GET failed with status:", response.status); } } catch (error) { console.error("Request error:", error.response?.status || error.message); } } getUsers(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('users', [ 'query' => [ 'page' => 0, 'sort' => '-id', ], ]); 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 ```json { "content": [ { "id": 1234, "name": "Michael Scott", "email": "michael@dundermifflin.com", "role": "admin" }, { "id": 1235, "name": "Jim Halpert", "email": "jimothy@dundermifflin.com", "role": "user" } ], "pagination_data": { "total_elements": 2, "total_pages": 1, "current_page_number": 1, "page_size": 50 } } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `content` | array[object] | List of users in the account | | └─`[].id` | integer | Unique identifier of the user | | └─`[].name` | string | Full name of the user | | └─`[].email` | string | Email address of the user | | └─`[].role` | string | User's role in the account (`admin` or `user`) | | `pagination_data` | object | Pagination information. See the [pagination section](#pagination) | Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Value of page is incorrect." | "Invalid sort parameter. Available values: +id/-id", "timestamp": "2025-03-05 17:57:00" } ``` An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ### Pagination The response body contains pagination details. It will support you in navigating through larger datasets. Each request returns 50 users. Use `page` parameter to view a specific page. :::info Page parameter is 0-based. Use `page=0` to retrieve the first one. `pagination_data.current_page_number` is 1-based. ::: ```json "pagination_data": { "total_elements": 80, "total_pages": 2, "current_page_number": 1, "page_size": 50 } ``` | Field | Type | Description | | ----------------------- | ------- | ----------------------------------- | | `pagination_data` | object | Pagination information | | └─`total_elements` | integer | Total number of users | | └─`total_pages` | integer | Total number of available pages | | └─`current_page_number` | integer | Current page number (1-based) | | └─`page_size` | integer | Maximum number of items per page | --- ## Delete domain Request deletion of an existing Woodpecker domain. Domain lookup is case-insensitive. The domain must belong to the account, must have `is_ready: true`, and must not already be in deletion process. ## Request You can delete a domain only when it is ready for deletion, which means it has `is_ready: true`. If the domain is not ready yet, the endpoint returns a validation error. :::warning Deleting a domain also deletes all mailboxes attached to that domain. After the deletion is processed, those mailboxes will no longer exist and cannot be recovered. ::: ### Endpoint ```text DELETE https://api.woodpecker.co/rest/v2/domains/{domain_name} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `domain_name` | Yes | string | Domain name | ### Request samples #### Delete a domain ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/domains/piedpiper.com" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def delete_domain(domain_name): url = f"https://api.woodpecker.co/rest/v2/domains/{domain_name}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.delete(url, headers=headers) if response.status_code == 202: return f"Domain deletion accepted: {response.status_code}" else: raise Exception(f"DELETE request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: result = delete_domain("piedpiper.com") print("DELETE response:", result) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String domainName = "piedpiper.com"; String url = "https://api.woodpecker.co/rest/v2/domains/" + domainName; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("DELETE response: Domain deletion accepted: " + response.statusCode()); } else { throw new Exception("DELETE request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function deleteDomain(domainName) { const url = `https://api.woodpecker.co/rest/v2/domains/${domainName}`; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.delete(url, { headers }); console.log('DELETE response:', `Domain deletion accepted: ${response.status}`); } catch (error) { console.error('DELETE request failed:', error.response ? error.response.status : error.message); } } deleteDomain('piedpiper.com'); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $domainName = 'piedpiper.com'; $response = $client->delete("domains/{$domainName}"); 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 The domain deletion has been accepted Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request body is valid JSON but contains invalid or missing fields. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "domain_name", "issue": "not_found", "value": "piedpiper.com" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Delete domain mailbox Request deletion of an existing mailbox from a domain. Domain and mailbox lookups are case-insensitive. The mailbox must belong to the account, must belong to the domain in the path, and must not already be in a deletion status. ## Request You can delete a mailbox only when it is ready for deletion, which means it has the `PAID` status. If the mailbox is not ready yet, the endpoint returns a validation error with the `cannot_delete_due_to_not_connected_mailbox` issue. :::warning After the deletion is processed, the mailbox will no longer exist and cannot be recovered. ::: ### Endpoint ```text DELETE https://api.woodpecker.co/rest/v2/domains/{domain_name}/mailboxes/{mailbox_email} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `domain_name` | Yes | string | Domain name | | `mailbox_email` | Yes | string | Mailbox email address. URL-encode it if your client does not encode path parameters automatically, for example `richard%40piedpiper.com` | ### Request samples #### Delete a mailbox ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/domains/piedpiper.com/mailboxes/richard%40piedpiper.com" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def delete_domain_mailbox(domain_name, mailbox_email): encoded_mailbox_email = requests.utils.quote(mailbox_email, safe="") url = f"https://api.woodpecker.co/rest/v2/domains/{domain_name}/mailboxes/{encoded_mailbox_email}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.delete(url, headers=headers) if response.status_code == 202: return f"Mailbox deletion accepted: {response.status_code}" else: raise Exception(f"DELETE request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: result = delete_domain_mailbox("piedpiper.com", "richard@piedpiper.com") print("DELETE response:", result) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; public class WoodpeckerApiClient { public static void main(String[] args) { try { String domainName = "piedpiper.com"; String mailboxEmail = "richard@piedpiper.com"; String encodedMailboxEmail = URLEncoder.encode(mailboxEmail, StandardCharsets.UTF_8); String url = "https://api.woodpecker.co/rest/v2/domains/" + domainName + "/mailboxes/" + encodedMailboxEmail; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .DELETE() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("DELETE response: Mailbox deletion accepted: " + response.statusCode()); } else { throw new Exception("DELETE request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function deleteDomainMailbox(domainName, mailboxEmail) { const encodedMailboxEmail = encodeURIComponent(mailboxEmail); const url = `https://api.woodpecker.co/rest/v2/domains/${domainName}/mailboxes/${encodedMailboxEmail}`; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.delete(url, { headers }); console.log('DELETE response:', `Mailbox deletion accepted: ${response.status}`); } catch (error) { console.error('DELETE request failed:', error.response ? error.response.status : error.message); } } deleteDomainMailbox('piedpiper.com', 'richard@piedpiper.com'); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $domainName = 'piedpiper.com'; $mailboxEmail = rawurlencode('richard@piedpiper.com'); $response = $client->delete("domains/{$domainName}/mailboxes/{$mailboxEmail}"); 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 The mailbox deletion has been accepted Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request body is valid JSON but contains invalid or missing fields. This also includes mailboxes that are not ready for deletion yet, for example a mailbox without the `PAID` status. In that case, the validation issue is `cannot_delete_due_to_not_connected_mailbox`. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "mailbox_email", "issue": "domain_mismatch", "value": "richard@other-domain.com" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue. Possible fields include `domain_name` and `mailbox_email` | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Domains The `/domains` API lets you **purchase domains and mailboxes that are preconfigured for cold emailing** and ready to use in Woodpecker. You can also check domain availability, save domain owner details, list ordered and purchased domains and mailboxes with their statuses, and manage domain-level settings such as redirect URLs and email forwarding. Before placing an order, use the provider and availability endpoints to check available options and pricing. Domain orders require domain owner details, so create or update them first if they are not saved in the account yet. After a domain is active, you can order additional mailboxes for it or update its redirect and forwarding settings. Some operations create billable assets. Domain and mailbox purchases are paid from prepaid funds. After purchase, mailbox renewals are covered by your regular monthly Woodpecker billing cycle. ## Purchase flow 1. Check your prepaid funds in Woodpecker. If the balance is too low, [add prepaid funds](https://app.woodpecker.co/panel#settings/billing/billing-information/add-funds) before placing an order. 2. Use [Get providers](get-providers.mdx) to review available providers and mailbox pricing, then use [Search domain availability](get-domain-availability.mdx) or [Check domain availability](post-domain-availability.mdx) to find domains you can buy. 3. Use [Get domain owner](get-owner.mdx) to check the saved owner details. If this is your first domain purchase or the details have changed, use [Save domain owner](post-owner.mdx) before ordering. 4. Place the order for domain and mailboxes with [Order domains](post-order-domains.mdx). To add mailboxes to an existing active domain, use [Order mailboxes](post-order-mailboxes.mdx). 5. Domain and mailbox orders are processed asynchronously. After the order is accepted, use [List domains](get-domains.mdx), [Get domain details](get-domain.mdx), [List domain mailboxes](get-domain-mailboxes.mdx), or [Get domain mailbox](get-domain-mailbox.mdx) to check whether the domain is ready and whether ordered mailboxes are paid and available. 6. After the domain and mailboxes are ready, adjust domain and mailbox settings if needed. Redirect URL and email forwarding can use defaults saved in the owner configuration, or you can update them with [Set redirect URL](patch-redirect-url.mdx) and [Set email forwarding](patch-email-forwarding.mdx). Use [Update mailbox profile picture](patch-mailbox-profile-picture.mdx) to set mailbox avatars. ## Available endpoints The table below lists every documented endpoint under `/rest/v2/domains`. | Endpoint | Method and path | Use it to | |----------|-----------------|-----------| | [Get providers](get-providers.mdx) | `GET /rest/v2/domains/providers` | Retrieve providers available for domain orders, including mailbox prices | | [Search domain availability](get-domain-availability.mdx) | `GET /rest/v2/domains/availability` | Search available domains by phrase | | [Check domain availability](post-domain-availability.mdx) | `POST /rest/v2/domains/availability` | Check a specific list of domains | | [Get domain owner](get-owner.mdx) | `GET /rest/v2/domains/owner` | Retrieve saved domain owner details | | [Save domain owner](post-owner.mdx) | `POST /rest/v2/domains/owner` | Create or update domain owner details | | [Order domains](post-order-domains.mdx) | `POST /rest/v2/domains/order` | Order domains and mailboxes together | | [Order mailboxes](post-order-mailboxes.mdx) | `POST /rest/v2/domains/mailboxes/order` | Order mailboxes for existing active domains | | [List domains](get-domains.mdx) | `GET /rest/v2/domains` | Retrieve domains that belong to your account | | [Get domain details](get-domain.mdx) | `GET /rest/v2/domains/{domain_name}` | Retrieve owner, settings, and mailbox summary for one domain | | [List domain mailboxes](get-domain-mailboxes.mdx) | `GET /rest/v2/domains/{domain_name}/mailboxes` | Retrieve mailboxes that belong to one domain | | [Get domain mailbox](get-domain-mailbox.mdx) | `GET /rest/v2/domains/{domain_name}/mailboxes/{mailbox_email}` | Retrieve mailbox details and connection settings | | [Update mailbox profile picture](patch-mailbox-profile-picture.mdx) | `PATCH /rest/v2/domains/{domain_name}/mailboxes/{mailbox_email}/profile_picture` | Update a mailbox profile picture | | [Set redirect URL](patch-redirect-url.mdx) | `PATCH /rest/v2/domains/{domain_name}/set_redirect_url` | Update the redirect URL for a domain | | [Set email forwarding](patch-email-forwarding.mdx) | `PATCH /rest/v2/domains/{domain_name}/set_email_forwarding` | Update the forwarding address for a domain | | [Delete domain](delete-domain.mdx) | `DELETE /rest/v2/domains/{domain_name}` | Delete a domain from Woodpecker | | [Delete mailbox](delete-mailbox.mdx) | `DELETE /rest/v2/domains/{domain_name}/mailboxes/{mailbox_email}` | Delete a mailbox from a domain | --- ## Search domain availability Search for available domain suggestions by phrase. The response includes available domain proposals and their prices. Use this endpoint when you want provider-generated suggestions before placing a domain order. Use the [check domain availability endpoint](/docs/domains/post-domain-availability.mdx) when you already have a list of domains you want to purchase. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/domains/availability ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `query` | Yes | string | Phrase to search for, for example `piedpiper`. | | `provider` | Yes | string | Provider that handles the search, for example `MAILDOSO`, `GOOGLE`, or `MICROSOFT`. | ### Request samples #### Search by phrase ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/domains/availability?query=piedpiper&provider=MAILDOSO" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def search_domain_availability(): url = "https://api.woodpecker.co/rest/v2/domains/availability" headers = { "x-api-key": "{YOUR_API_KEY}" } params = {"query": "piedpiper", "provider": "MAILDOSO"} response = requests.get(url, headers=headers, params=params) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = search_domain_availability() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/domains/availability?query=piedpiper&provider=MAILDOSO"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new Exception("GET request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function searchDomainAvailability() { const url = 'https://api.woodpecker.co/rest/v2/domains/availability'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; const params = { query: 'piedpiper', provider: 'MAILDOSO' }; try { const response = await axios.get(url, { headers, params }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } searchDomainAvailability(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('domains/availability', [ 'query' => ['query' => 'piedpiper', 'provider' => 'MAILDOSO'], ]); 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 Request processed successfully. Returns available domain suggestions with pricing for the selected provider. Returns an empty array if there are no results. ```json { "provider": "MAILDOSO", "query": "piedpiper", "domains": [ { "domain": "piedpiper.com", "available": true, "price": { "amount": "13.00", "currency": "USD" } } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `provider` | string | Provider used for the availability search | | `query` | string | Search phrase from the request | | `domains` | array | Domain availability results. Empty array if there are no results | |   └─ `domain` | string | Domain name | |   └─ `available` | boolean | Whether the domain is available | |   └─ `price` | object or null | Domain price when available | |   └─ `price.amount` | string | Price amount | |   └─ `price.currency` | string | Price currency | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request has invalid or missing parameters. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "1b752aaa-b067-4145-8e22-939582dfeb2c", "fields": [ { "field": "provider", "issue": "Valid values are MAILDOSO, MAILFORGE, GOOGLE, MICROSOFT", "value": "INVALID" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Get domain mailbox Retrieve details of a single mailbox under a purchased domain, including general mailbox information, credentials, and IMAP/SMTP settings. To get a list of all mailboxes under a domain, use the [list domain mailboxes](get-domain-mailboxes.mdx) endpoint. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/domains/{domain_name}/mailboxes/{mailbox_email} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `domain_name` | Yes | string | Full domain name | | `mailbox_email` | Yes | string | Mailbox email address. The mailbox must belong to the domain in `domain_name`. URL-encode it if your client does not encode path parameters automatically, for example `richard%40piedpiper.com` | ### Request samples #### Get a mailbox ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/domains/piedpiper.com/mailboxes/richard%40piedpiper.com" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_domain_mailbox(domain_name, mailbox_email): encoded_mailbox_email = requests.utils.quote(mailbox_email, safe="") url = f"https://api.woodpecker.co/rest/v2/domains/{domain_name}/mailboxes/{encoded_mailbox_email}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_domain_mailbox("piedpiper.com", "richard@piedpiper.com") print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; public class WoodpeckerApiClient { public static void main(String[] args) { try { String domainName = "piedpiper.com"; String mailboxEmail = "richard@piedpiper.com"; String encodedMailboxEmail = URLEncoder.encode(mailboxEmail, StandardCharsets.UTF_8); String url = "https://api.woodpecker.co/rest/v2/domains/" + domainName + "/mailboxes/" + encodedMailboxEmail; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new Exception("GET request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function getDomainMailbox(domainName, mailboxEmail) { const encodedMailboxEmail = encodeURIComponent(mailboxEmail); const url = `https://api.woodpecker.co/rest/v2/domains/${domainName}/mailboxes/${encodedMailboxEmail}`; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getDomainMailbox('piedpiper.com', 'richard@piedpiper.com'); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $domainName = 'piedpiper.com'; $mailboxEmail = rawurlencode('richard@piedpiper.com'); $response = $client->get("domains/{$domainName}/mailboxes/{$mailboxEmail}"); 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 Request processed successfully ```json { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com", "login": "richard@piedpiper.com", "provider": "MAILDOSO", "status": "PAID", "authenticator": null, "smtp_server": "smtp.example.com", "smtp_password": "secret", "smtp_port": 587, "imap_server": "imap.example.com", "imap_email": "richard@piedpiper.com", "imap_password": "secret", "imap_port": 993, "created": "2026-06-24T10:00:00", "profile_picture": "https://avatars.example.com/users/40/avatar.png" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `first_name` | string | Mailbox display first name | | `last_name` | string | Mailbox display last name | | `email` | string | Mailbox email address | | `login` | string | Login used for the mailbox | | `provider` | string | Public provider name | | `status` | string | Mailbox status. Possible values are `ORDERED` and `PAID` | | `authenticator` | string or null | Current one-time authenticator code when available | | `smtp_server` | string | SMTP server hostname | | `smtp_password` | string | SMTP password | | `smtp_port` | integer | SMTP port number | | `imap_server` | string | IMAP server hostname | | `imap_email` | string | IMAP email address | | `imap_password` | string | IMAP password | | `imap_port` | integer | IMAP port number| | `created` | string | Mailbox creation timestamp | | `profile_picture` | string or null | Profile picture URL | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | The domain or mailbox was not found, belongs to another account, or is deleted. The response body is empty Returned when the request body is valid JSON but contains invalid or missing fields. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "mailbox_email", "issue": "domain_mismatch", "value": "richard@other-domain.com" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue. Possible fields include `domain_name` and `mailbox_email` | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## List domain mailboxes Retrieve active mailboxes for a purchased domain, including basic mailbox information. For credentials and connection settings, use the [get domain mailbox](get-domain-mailbox.mdx) endpoint. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/domains/{domain_name}/mailboxes ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `domain_name` | Yes | string | Full domain name | ### Request samples #### List mailboxes ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/domains/piedpiper.com/mailboxes" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def list_domain_mailboxes(domain_name): url = f"https://api.woodpecker.co/rest/v2/domains/{domain_name}/mailboxes" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = list_domain_mailboxes("piedpiper.com") print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String domainName = "piedpiper.com"; String url = "https://api.woodpecker.co/rest/v2/domains/" + domainName + "/mailboxes"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new Exception("GET request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function listDomainMailboxes(domainName) { const url = `https://api.woodpecker.co/rest/v2/domains/${domainName}/mailboxes`; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } listDomainMailboxes('piedpiper.com'); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $domainName = 'piedpiper.com'; $response = $client->get("domains/{$domainName}/mailboxes"); 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 Request processed successfully. If there are no mailboxes under this domain, `mailboxes` will be an empty array. ```json { "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com", "provider": "GOOGLE", "status": "PAID" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `mailboxes` | array | Mailboxes on the domain | |   └─ `first_name` | string | Mailbox owner's first name | |   └─ `last_name` | string | Mailbox owner's last name | |   └─ `email` | string | Mailbox email address | |   └─ `provider` | string | Public provider name | |   └─ `status` | string | Mailbox status. Possible values are `ORDERED` and `PAID` | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | The domain was not found, belongs to another account, or is deleted. The response body is empty Returned when the request has invalid or missing parameters. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "domain_name", "issue": "required", "value": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue. Possible fields include `domain_name` | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Get domain details Retrieve details for a single purchased domain, including owner data, forwarding settings, readiness status, and mailbox summary. To retrieve all purchased domains that belong to your account, use the [list domains](get-domains.mdx) endpoint. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/domains/{domain_name} ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `domain_name` | Yes | string | Full domain name | ### Request samples #### Get a domain ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/domains/piedpiper.com" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_domain(domain_name): url = f"https://api.woodpecker.co/rest/v2/domains/{domain_name}" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_domain("piedpiper.com") print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String domainName = "piedpiper.com"; String url = "https://api.woodpecker.co/rest/v2/domains/" + domainName; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new Exception("GET request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function getDomain(domainName) { const url = `https://api.woodpecker.co/rest/v2/domains/${domainName}`; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getDomain('piedpiper.com'); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $domainName = 'piedpiper.com'; $response = $client->get("domains/{$domainName}"); 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 Request processed successfully ```json { "owner": { "email": "owner@piedpiper.com", "first_name": "Richard", "last_name": "Hendricks", "company_name": "Pied Piper", "address": "5230 Newell Road", "city": "Palo Alto", "state": "CA", "country": "US", "zip_code": "94303", "phone": "+1 650 555 0100" }, "email_forward_address": "forward@piedpiper.com", "domain_redirect_url": "https://piedpiper.com", "expires": "2027-06-24T10:00:00", "created": "2026-06-24T10:00:00", "is_custom_domain": true, "is_ready": true, "provider": "GOOGLE", "mailboxes_count": 2, "max_mailboxes": 10, "all_mailboxes_are_ready": false } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `owner` | object | Domain owner details | |   └─ `email` | string | Owner email address | |   └─ `first_name` | string | Owner first name | |   └─ `last_name` | string | Owner last name | |   └─ `company_name` | string or null | Owner company name | |   └─ `address` | string | Owner street address | |   └─ `city` | string | Owner city | |   └─ `state` | string | Owner state or region | |   └─ `country` | string | Owner country code | |   └─ `zip_code` | string | Owner postal code | |   └─ `phone` | string | Owner phone number | | `email_forward_address` | string or null | Domain forwarding email address | | `domain_redirect_url` | string or null | Domain redirect URL | | `expires` | string | Domain expiration timestamp | | `created` | string | Domain creation timestamp | | `is_custom_domain` | boolean | Whether the domain is marked as custom | | `is_ready` | boolean | Whether the domain is ready to use | | `provider` | string | Public provider name | | `mailboxes_count` | integer | Number of non-deleted mailboxes on the domain | | `max_mailboxes` | integer | Maximum number of mailboxes allowed on the domain | | `all_mailboxes_are_ready` | boolean | Whether every non-deleted mailbox on the domain is ready | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | The domain was not found, belongs to another account, or is deleted. The response body is empty Returned when the request body is valid JSON but contains invalid or missing fields. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "domain_name", "issue": "required", "value": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue. Possible fields include `domain_name` | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## List domains Retrieve all purchased domains that belong to your account, including their status, expiration date, mailbox count, and other basic information. To retrieve full details for a specific domain, use the [get domain endpoint](get-domain.mdx). ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/domains ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### List domains ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/domains" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def list_domains(): url = "https://api.woodpecker.co/rest/v2/domains" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = list_domains() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/domains"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new Exception("GET request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function listDomains() { const url = 'https://api.woodpecker.co/rest/v2/domains'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } listDomains(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('domains'); 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 Request processed successfully. If there are no domains under this account, `domains` will be an empty array. ```json { "domains": [ { "domain": "piedpiper.com", "mailboxes_count": 2, "provider": "GOOGLE", "is_custom_domain": true, "is_ready": true, "expires": "2027-06-24T10:00:00", "created": "2026-06-24T10:00:00" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `domains` | array | Domains that belong to the account | |   └─ `domain` | string | Domain name | |   └─ `mailboxes_count` | integer | Number of non-deleted mailboxes associated with the domain | |   └─ `provider` | string | Domain and mailboxes provider name. Check [providers endpoint](/docs/domains/get-providers.mdx) to list available providers | |   └─ `is_custom_domain` | boolean | Whether the domain was purchased independently and transferred to Woodpecker | |   └─ `is_ready` | boolean | Whether the domain is ready to use | |   └─ `expires` | string | Domain expiration timestamp | |   └─ `created` | string | Domain creation timestamp | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Get domain owner Retrieve the domain owner details saved for the account. These details are required before ordering domains and are used to register purchased domains. ## Request ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/domains/owner ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Retrieve domain owner ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/domains/owner" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def retrieve_domain_owner(): url = "https://api.woodpecker.co/rest/v2/domains/owner" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = retrieve_domain_owner() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/domains/owner"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new Exception("GET request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function retrieveDomainOwner() { const url = 'https://api.woodpecker.co/rest/v2/domains/owner'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } retrieveDomainOwner(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('domains/owner'); 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 Request processed successfully. If the account does not have saved owner details yet, the endpoint returns an object with all values set to `null` ```json { "email": "richard@piedpiper.com", "first_name": "Richard", "last_name": "Hendricks", "company_name": "Pied Piper", "address": "123 Street", "city": "Palo Alto", "state": "CA", "country": "US", "zip_code": "943032", "phone": "+1 100 200 3000", "configuration": { "default_domain_redirect_url": "https://piedpiper.com", "default_email_forward_address": "forward@piedpiper.com" } } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `email` | string/null | Owner email address | | `first_name` | string/null | Owner first name | | `last_name` | string/null | Owner last name | | `company_name` | string/null | Company name | | `address` | string/null | Owner address | | `city` | string/null | Owner city | | `state` | string/null | Owner state or region | | `country` | string/null | Owner country code | | `zip_code` | string/null | Owner postal code | | `phone` | string/null | Owner phone number | | `configuration` | object | Default domain settings | |   └─ `default_domain_redirect_url` | string/null | Default redirect URL for ordered domains | |   └─ `default_email_forward_address` | string/null | Default forwarding address for ordered domains | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Get domain providers Retrieve providers available for domain and mailbox orders. The response includes the mailbox price for each provider in the account currency. ## Request Provider prices will affect billing when you place a domain or mailbox order. ### Endpoint ```text GET https://api.woodpecker.co/rest/v2/domains/providers ``` ### Headers ```text x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Retrieve providers ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/domains/providers" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def retrieve_providers(): url = "https://api.woodpecker.co/rest/v2/domains/providers" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = retrieve_providers() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/domains/providers"; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { throw new Exception("GET request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function retrieveProviders() { const url = 'https://api.woodpecker.co/rest/v2/domains/providers'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } retrieveProviders(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('domains/providers'); 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 Request processed successfully ```json { "providers": [ { "name": "MAILDOSO", "currency": "EUR", "email_account_price": 4.00 }, { "name": "MAILFORGE", "currency": "USD", "email_account_price": 4.0 }, { "name": "GOOGLE", "currency": "EUR", "email_account_price": 6.00 }, { "name": "MICROSOFT", "currency": "EUR", "email_account_price": 6.00 } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `providers` | array | Providers available for orders | |   └─ `name` | string | Public provider name | |   └─ `currency` | string | Currency used for the price | |   └─ `email_account_price` | number | Price of one email account for this provider | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Set domain email forwarding Set the forwarding email address for an existing domain. All emails sent to mailboxes under the domain will be automatically forwarded to the selected address. You can set it only after the domain is ready to use, meaning that [get domain endpoint](get-domain.mdx) returns `is_ready` as true. ## Request ### Endpoint ```text PATCH https://api.woodpecker.co/rest/v2/domains/{domain_name}/set_email_forwarding ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `domain_name` | Yes | string | Domain name | ### Body ```json { "email_forward_address": "forward@piedpiper.com" } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `email_forward_address` | string | Yes | Forwarding email address. Use an empty string to remove the existing forwarding address. A non-empty value must be a valid email address | ### Request samples #### Set email forwarding ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/domains/piedpiper.com/set_email_forwarding" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "email_forward_address": "forward@piedpiper.com" }' ``` ```Python import requests def set_email_forwarding(domain_name): url = f"https://api.woodpecker.co/rest/v2/domains/{domain_name}/set_email_forwarding" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "email_forward_address": "forward@piedpiper.com" } response = requests.patch(url, headers=headers, json=payload) if response.status_code == 202: return f"Email forwarding update accepted: {response.status_code}" else: raise Exception(f"PATCH request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: result = set_email_forwarding("piedpiper.com") print("PATCH response:", result) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String domainName = "piedpiper.com"; String url = "https://api.woodpecker.co/rest/v2/domains/" + domainName + "/set_email_forwarding"; String jsonData = """ { "email_forward_address": "forward@piedpiper.com" } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("PATCH response: Email forwarding update accepted: " + response.statusCode()); } else { throw new Exception("PATCH request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function setEmailForwarding(domainName) { const url = `https://api.woodpecker.co/rest/v2/domains/${domainName}/set_email_forwarding`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { email_forward_address: 'forward@piedpiper.com' }; try { const response = await axios.patch(url, data, { headers }); console.log('PATCH response:', `Email forwarding update accepted: ${response.status}`); } catch (error) { console.error('PATCH request failed:', error.response ? error.response.status : error.message); } } setEmailForwarding('piedpiper.com'); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $domainName = 'piedpiper.com'; $response = $client->patch("domains/{$domainName}/set_email_forwarding", [ 'json' => [ 'email_forward_address' => 'forward@piedpiper.com', ], ]); 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 The email forwarding update has been accepted Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | The requested resource or path does not exist ```json { "title": "Not Found", "status": 404, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Returned when the request body is valid JSON but contains invalid or missing fields. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "email_forward_address", "issue": "required", "value": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue. Possible fields include `domain_name` and `email_forward_address` | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Update mailbox profile picture Add or update the profile picture for an existing mailbox. This helps recipients recognize the sender more easily. Currently, this feature is supported for Google mailboxes. ## Request ### Endpoint ```text PATCH https://api.woodpecker.co/rest/v2/domains/{domain_name}/mailboxes/{mailbox_email}/profile_picture ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `domain_name` | Yes | string | Domain name | | `mailbox_email` | Yes | string | Mailbox email address. The mailbox must belong to the domain in `domain_name`. URL-encode the mailbox email address, for example `richard%40piedpiper.com` | ### Body ```json { "avatar": "ikAtLQMKKAFooAWkoELVrT4w92..." } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `avatar` | string | Yes | PNG or JPEG image encoded as Base64. Send only the Base64 content, without a Data URL prefix such as `data:image/png;base64,` | The decoded image can be up to 5 MB or `4096x4096` pixels. We recommend using smaller, lightweight images when possible. ### Request samples #### Update profile picture ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/domains/piedpiper.com/mailboxes/richard%40piedpiper.com/profile_picture" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "avatar": "iVBORw0KGgoAAAANSUhEUgAA..." }' ``` ```Python import requests def update_mailbox_profile_picture(domain_name, mailbox_email): encoded_mailbox_email = requests.utils.quote(mailbox_email, safe="") url = f"https://api.woodpecker.co/rest/v2/domains/{domain_name}/mailboxes/{encoded_mailbox_email}/profile_picture" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "avatar": "iVBORw0KGgoAAAANSUhEUgAA..." } response = requests.patch(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"PATCH request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = update_mailbox_profile_picture("piedpiper.com", "richard@piedpiper.com") print("PATCH response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; public class WoodpeckerApiClient { public static void main(String[] args) { try { String domainName = "piedpiper.com"; String mailboxEmail = "richard@piedpiper.com"; String encodedMailboxEmail = URLEncoder.encode(mailboxEmail, StandardCharsets.UTF_8); String url = "https://api.woodpecker.co/rest/v2/domains/" + domainName + "/mailboxes/" + encodedMailboxEmail + "/profile_picture"; String jsonData = """ { "avatar": "iVBORw0KGgoAAAANSUhEUgAA..." } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("PATCH response: " + response.body()); } else { throw new Exception("PATCH request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function updateMailboxProfilePicture(domainName, mailboxEmail) { const encodedMailboxEmail = encodeURIComponent(mailboxEmail); const url = `https://api.woodpecker.co/rest/v2/domains/${domainName}/mailboxes/${encodedMailboxEmail}/profile_picture`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { avatar: 'iVBORw0KGgoAAAANSUhEUgAA...' }; try { const response = await axios.patch(url, data, { headers }); console.log('PATCH response:', response.data); } catch (error) { console.error('PATCH request failed:', error.response ? error.response.status : error.message); } } updateMailboxProfilePicture('piedpiper.com', 'richard@piedpiper.com'); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $domainName = 'piedpiper.com'; $mailboxEmail = rawurlencode('richard@piedpiper.com'); $response = $client->patch("domains/{$domainName}/mailboxes/{$mailboxEmail}/profile_picture", [ 'json' => [ 'avatar' => 'iVBORw0KGgoAAAANSUhEUgAA...', ], ]); 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 The profile picture has been updated ```json { "success": true } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `success` | boolean | Whether the profile picture update succeeded | Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request body is valid JSON but contains invalid or missing fields. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "avatar", "issue": "must be a valid Base64 encoded file", "value": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue. Possible fields include `domain_name`, `mailbox_email`, and `avatar` | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Set domain redirect URL Set the redirect URL for an existing domain where visitors will be redirected when they open your domain. You can set it only after the domain is ready to use, meaning that [get domain endpoint](get-domain.mdx) returns `is_ready` as true. ## Request ### Endpoint ```text PATCH https://api.woodpecker.co/rest/v2/domains/{domain_name}/set_redirect_url ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `domain_name` | Yes | string | Domain name | ### Body ```json { "domain_redirect_url": "https://example.com" } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `domain_redirect_url` | string | Yes | Redirect URL. Use an empty string to remove an existing redirect. A non-empty value must start with `http://` or `https://` | ### Request samples #### Set redirect URL ```bash curl --request PATCH \ --url "https://api.woodpecker.co/rest/v2/domains/example.com/set_redirect_url" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "domain_redirect_url": "https://example.com" }' ``` ```Python import requests def set_redirect_url(domain_name): url = f"https://api.woodpecker.co/rest/v2/domains/{domain_name}/set_redirect_url" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "domain_redirect_url": "https://example.com" } response = requests.patch(url, headers=headers, json=payload) if response.status_code == 202: return f"Redirect URL update accepted: {response.status_code}" else: raise Exception(f"PATCH request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: result = set_redirect_url("example.com") print("PATCH response:", result) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String domainName = "example.com"; String url = "https://api.woodpecker.co/rest/v2/domains/" + domainName + "/set_redirect_url"; String jsonData = """ { "domain_redirect_url": "https://example.com" } """; HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .header("x-api-key", "{YOUR_API_KEY}") .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(jsonData)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("PATCH response: Redirect URL update accepted: " + response.statusCode()); } else { throw new Exception("PATCH request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function setRedirectUrl(domainName) { const url = `https://api.woodpecker.co/rest/v2/domains/${domainName}/set_redirect_url`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { domain_redirect_url: 'https://example.com' }; try { const response = await axios.patch(url, data, { headers }); console.log('PATCH response:', `Redirect URL update accepted: ${response.status}`); } catch (error) { console.error('PATCH request failed:', error.response ? error.response.status : error.message); } } setRedirectUrl('example.com'); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $domainName = 'example.com'; $response = $client->patch("domains/{$domainName}/set_redirect_url", [ 'json' => [ 'domain_redirect_url' => 'https://example.com', ], ]); 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 The redirect URL update has been accepted Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request body is valid JSON but contains invalid or missing fields. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "domain_name", "issue": "not_found", "value": "example.com" }, { "field": "domain_redirect_url", "issue": "Valid format is http://example.com or https://example.com", "value": "htt://xxxxx" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue. Possible fields include `domain_name` and `domain_redirect_url` | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Check domain availability Check availability for a specific list of domains with a selected provider. The response includes each domain's availability status and price. Use this endpoint when you already have exact domain names to verify before placing an order. Use the [search domain availability endpoint](get-domain-availability.mdx) when you have domain keywords and want provider-generated domain proposals. ## Request ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/domains/availability ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Parameters Check currently available providers with the [providers endpoint](/docs/domains/get-providers.mdx) before sending requests. Provider availability can change, and that endpoint is the source of truth. | Parameter | Required | Type | Description | |-----------|:--------:|------|-------------| | `provider` | Yes | string | Provider that handles the check, for example `MAILDOSO`, `MAILFORGE`, `GOOGLE`, or `MICROSOFT` | ### Body ```json { "domains": [ "piedpiper.com", "piedpiper.io" ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `domains` | array[string] | Yes | Domain names to check | ### Request samples #### Check domains ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/domains/availability?provider=MAILDOSO" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "domains": [ "piedpiper.com", "piedpiper.io" ] }' ``` ```Python import requests def check_domain_availability(): url = "https://api.woodpecker.co/rest/v2/domains/availability" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } params = {"provider": "MAILDOSO"} payload = {"domains": ["piedpiper.com", "piedpiper.io"]} response = requests.post(url, headers=headers, params=params, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = check_domain_availability() print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/domains/availability?provider=MAILDOSO"; String jsonData = """ { "domains": [ "piedpiper.com", "piedpiper.io" ] } """; 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { throw new Exception("POST request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function checkDomainAvailability() { const url = 'https://api.woodpecker.co/rest/v2/domains/availability'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json', }; const params = { provider: 'MAILDOSO' }; const data = { domains: ['piedpiper.com', 'piedpiper.io'] }; try { const response = await axios.post(url, data, { headers, params }); console.log('POST response:', response.data); } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } checkDomainAvailability(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('domains/availability', [ 'query' => ['provider' => 'MAILDOSO'], 'json' => ['domains' => ['piedpiper.com', 'piedpiper.io']], ]); 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 Request processed successfully. Unavailable domains return `price: null`. Returns an empty array if there are no results. ```json { "provider": "MAILDOSO", "domains": [ { "domain": "piedpiper.com", "available": true, "price": { "amount": "13.00", "currency": "USD" } }, { "domain": "piedpiper.io", "available": false, "price": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `provider` | string | Provider used for the availability check | | `domains` | array | Domain availability results | |   └─ `domain` | string | Domain name | |   └─ `available` | boolean | Whether the domain is available | |   └─ `price` | object or null | Domain price when available | |   └─ `price.amount` | string | Price amount | |   └─ `price.currency` | string | Price currency | Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request body is valid JSON but contains invalid or missing fields. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "1b752aaa-b067-4145-8e22-939582dfeb2c", "fields": [ { "field": "domains", "issue": "required", "value": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Order domains Order domains and mailboxes that are preconfigured for cold emailing and ready to use in Woodpecker. [Creating a domain owner](/docs/domains/post-owner.mdx) is required before placing an order. Domain and mailbox purchases are paid from your [prepaid funds](https://woodpecker.co/help-center/en/articles/15691583), so you need enough balance before placing an order. After purchase, mailbox renewals are covered by your regular monthly Woodpecker billing cycle. If the order fails because of insufficient funds, [add prepaid funds in Woodpecker](https://app.woodpecker.co/panel#settings/billing/billing-information/add-funds) before retrying. ## Request :::info This endpoint will create billable domain and mailbox assets ::: Use the [get domain providers endpoint](get-providers.mdx) to check mailbox pricing, and an availability endpoint to check domain pricing and availability before placing an order. The purchase itself is paid from prepaid funds; ongoing mailbox renewals are billed monthly after purchase. ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/domains/order ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "provider": "GOOGLE", "domains": [ "piedpiper.com" ], "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com" } ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `provider` | string | Yes | Provider used for the order. Valid values include `MAILDOSO`, `MAILFORGE`, `GOOGLE`, and `MICROSOFT`. Check [providers endpoint](/docs/domains/get-providers.mdx) to list available providers | | `domains` | array[string] | Yes | Domains to order. Values must be unique and must not already exist for the account. Use [order mailboxes endpoint](/docs/domains/post-order-mailboxes.mdx) to purchase mailboxes for an already existing domain | | `mailboxes` | array | Yes | Mailboxes to order with the domains | |   └─ `first_name` | string | Yes | Mailbox owner's first name. Used to display the 'from name' | |   └─ `last_name` | string | Yes | Mailbox owner's last name. Used to display the 'from name' | |   └─ `email` | string | Yes | Mailbox email address to order | ### Request samples #### Order a domain with one mailbox ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/domains/order" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "provider": "GOOGLE", "domains": [ "piedpiper.com" ], "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com" } ] }' ``` ```Python import requests def order_domains(): url = "https://api.woodpecker.co/rest/v2/domains/order" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "provider": "GOOGLE", "domains": [ "piedpiper.com" ], "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com" } ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 202: return f"Domain order accepted: {response.status_code}" else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: result = order_domains() print("POST response:", result) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/domains/order"; String jsonData = """ { "provider": "GOOGLE", "domains": [ "piedpiper.com" ], "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com" } ] } """; 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("POST response: Domain order accepted: " + response.statusCode()); } else { throw new Exception("POST request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function orderDomains() { const url = 'https://api.woodpecker.co/rest/v2/domains/order'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { provider: 'GOOGLE', domains: [ 'piedpiper.com' ], mailboxes: [ { first_name: 'Richard', last_name: 'Hendricks', email: 'richard@piedpiper.com' } ] }; try { const response = await axios.post(url, data, { headers }); console.log('POST response:', `Domain order accepted: ${response.status}`); } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } orderDomains(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('domains/order', [ 'json' => [ 'provider' => 'GOOGLE', 'domains' => [ 'piedpiper.com', ], 'mailboxes' => [ [ 'first_name' => 'Richard', 'last_name' => 'Hendricks', 'email' => 'richard@piedpiper.com', ], ], ], ]); 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 The order has been accepted for processing. Use the [get domain endpoint](get-domain.mdx) to review the processing status. Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request body is valid JSON but contains invalid or missing fields, or when business validation fails before the order can be accepted. If `code` is `insufficient_funds`, [add prepaid funds in Woodpecker](https://app.woodpecker.co/panel#settings/billing/billing-information/add-funds) before retrying the order. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "provider", "issue": "Valid values are MAILDOSO, MAILFORGE, GOOGLE, MICROSOFT", "value": "MAILDOSO1" }, { "field": "domains[0]", "issue": "Valid format is example.com", "value": "invalid_domain" }, { "field": "mailboxes[0].email", "issue": "Valid format is email@example.com", "value": "invalid" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code. Possible business error values include `domain_owner_required`, `insufficient_funds`, `payment_failed`, `domain_unavailable`, and `domain_price_unavailable` | | `request_id` | string or null | Request identifier when available | | `fields` | array/null | Fields that failed validation. Present for field-level validation errors | |   └─ `field` | string | Field with a validation issue | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Order domain mailboxes Order mailboxes for domains you already purchased in Woodpecker. The mailboxes are preconfigured for cold emailing and ready to use in Woodpecker. To purchase a new domain together with mailboxes, use the [order domains endpoint](/docs/domains/post-order-domains.mdx). ## Request :::info This endpoint will create billable mailbox assets ::: Mailbox purchases are paid from your [prepaid funds](https://woodpecker.co/help-center/en/articles/15691583), so you need enough balance before placing an order. After purchase, mailbox renewals are covered by your regular monthly Woodpecker billing cycle. Use the [get domain providers endpoint](get-providers.mdx) to check mailbox pricing before placing an order. ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/domains/mailboxes/order ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com" } ] } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `mailboxes` | array | Yes | Mailboxes to order for existing active domains | |   └─ `first_name` | string | Yes | Mailbox owner's first name. Used to display the 'from name' | |   └─ `last_name` | string | Yes | Mailbox owner's last name. Used to display the 'from name' | |   └─ `email` | string | Yes | Mailbox email address to order | ### Request samples #### Order a mailbox ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/domains/mailboxes/order" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com" } ] }' ``` ```Python import requests def order_mailboxes(): url = "https://api.woodpecker.co/rest/v2/domains/mailboxes/order" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com" } ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 202: return f"Mailbox order accepted: {response.status_code}" else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: result = order_mailboxes() print("POST response:", result) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/domains/mailboxes/order"; String jsonData = """ { "mailboxes": [ { "first_name": "Richard", "last_name": "Hendricks", "email": "richard@piedpiper.com" } ] } """; 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("POST response: Mailbox order accepted: " + response.statusCode()); } else { throw new Exception("POST request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function orderMailboxes() { const url = 'https://api.woodpecker.co/rest/v2/domains/mailboxes/order'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { mailboxes: [ { first_name: 'Richard', last_name: 'Hendricks', email: 'richard@piedpiper.com' } ] }; try { const response = await axios.post(url, data, { headers }); console.log('POST response:', `Mailbox order accepted: ${response.status}`); } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } orderMailboxes(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('domains/mailboxes/order', [ 'json' => [ 'mailboxes' => [ [ 'first_name' => 'Richard', 'last_name' => 'Hendricks', 'email' => 'richard@piedpiper.com', ], ], ], ]); 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 The order has been accepted for processing. Use the [list domain mailboxes endpoint](get-domain-mailboxes.mdx) to review the processing status. Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request body is valid JSON but contains invalid or missing fields, or when business validation fails before the order can be accepted. If `code` is `insufficient_funds`, [add prepaid funds in Woodpecker](https://app.woodpecker.co/panel#settings/billing/billing-information/add-funds) before retrying the order. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "mailboxes[0].email", "issue": "Email domain must exist in order domains or active firm domains", "value": "richard@missing.com" } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code. Possible business error values include `domain_owner_required`, `insufficient_funds`, and `payment_failed` | | `request_id` | string or null | Request identifier when available | | `fields` | array/null | Fields that failed validation. Present for field-level validation errors | |   └─ `field` | string | Field with a validation issue | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Save domain owner Create or update the domain owner details for the account. A domain owner is required before you can [order domains](post-order-domains.mdx) to register them. This endpoint replaces the saved owner details, so the request body must include the complete domain owner object. ## Request ### Endpoint ```text POST https://api.woodpecker.co/rest/v2/domains/owner ``` ### Headers ```text x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "email": "owner@piedpiper.com", "first_name": "Richard", "last_name": "Hendricks", "company_name": "Pied Piper", "address": "5230 Newell Road", "city": "Palo Alto", "state": "CA", "country": "US", "zip_code": "94303", "phone": "+16505550100", "configuration": { "default_domain_redirect_url": "https://piedpiper.com", "default_email_forward_address": "forward@piedpiper.com" } } ``` #### Body schema | Field | Type | Required | Description | |-------|------|:--------:|-------------| | `email` | string | Yes | Owner email address | | `first_name` | string | Yes | Owner first name | | `last_name` | string | Yes | Owner last name | | `company_name` | string | No | Company name | | `address` | string | Yes | Owner address | | `city` | string | Yes | Owner city | | `state` | string | Yes | Owner state or region | | `country` | string | Yes | Owner country | | `zip_code` | string | Yes | Postal code in the format valid for the selected country | | `phone` | string | Yes | Phone number valid for the selected country | | `configuration` | object | No | Default settings for ordered domains | |   └─ `default_domain_redirect_url` | string | No | Default domain redirect URL. If not empty, it must start with `http://` or `https://` | |   └─ `default_email_forward_address` | string | No | Default email forwarding address. If not empty, it must be a valid email address | ### Request samples #### Save domain owner ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/domains/owner" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "email": "owner@piedpiper.com", "first_name": "Richard", "last_name": "Hendricks", "company_name": "Pied Piper", "address": "5230 Newell Road", "city": "Palo Alto", "state": "CA", "country": "US", "zip_code": "94303", "phone": "+16505550100", "configuration": { "default_domain_redirect_url": "https://piedpiper.com", "default_email_forward_address": "forward@piedpiper.com" } }' ``` ```Python import requests def save_domain_owner(): url = "https://api.woodpecker.co/rest/v2/domains/owner" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "email": "owner@piedpiper.com", "first_name": "Richard", "last_name": "Hendricks", "company_name": "Pied Piper", "address": "5230 Newell Road", "city": "Palo Alto", "state": "CA", "country": "US", "zip_code": "94303", "phone": "+16505550100", "configuration": { "default_domain_redirect_url": "https://piedpiper.com", "default_email_forward_address": "forward@piedpiper.com" } } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = save_domain_owner() print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class WoodpeckerApiClient { public static void main(String[] args) { try { String url = "https://api.woodpecker.co/rest/v2/domains/owner"; String jsonData = """ { "email": "owner@piedpiper.com", "first_name": "Richard", "last_name": "Hendricks", "company_name": "Pied Piper", "address": "5230 Newell Road", "city": "Palo Alto", "state": "CA", "country": "US", "zip_code": "94303", "phone": "+16505550100", "configuration": { "default_domain_redirect_url": "https://piedpiper.com", "default_email_forward_address": "forward@piedpiper.com" } } """; 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { throw new Exception("POST request failed: " + response.statusCode() + ", " + response.body()); } } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } } ``` ```js const axios = require('axios'); async function saveDomainOwner() { const url = 'https://api.woodpecker.co/rest/v2/domains/owner'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json', }; const data = { email: 'owner@piedpiper.com', first_name: 'Richard', last_name: 'Hendricks', company_name: 'Pied Piper', address: '5230 Newell Road', city: 'Palo Alto', state: 'CA', country: 'US', zip_code: '94303', phone: '+16505550100', configuration: { default_domain_redirect_url: 'https://piedpiper.com', default_email_forward_address: 'forward@piedpiper.com', }, }; 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); } } saveDomainOwner(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('domains/owner', [ 'json' => [ 'email' => 'owner@piedpiper.com', 'first_name' => 'Richard', 'last_name' => 'Hendricks', 'company_name' => 'Pied Piper', 'address' => '5230 Newell Road', 'city' => 'Palo Alto', 'state' => 'CA', 'country' => 'US', 'zip_code' => '94303', 'phone' => '+16505550100', 'configuration' => [ 'default_domain_redirect_url' => 'https://piedpiper.com', 'default_email_forward_address' => 'forward@piedpiper.com', ], ], ]); 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 Request processed successfully. Owner created or updated ```json { "email": "owner@piedpiper.com", "first_name": "Richard", "last_name": "Hendricks", "company_name": "Pied Piper", "address": "5230 Newell Road", "city": "Palo Alto", "state": "CA", "country": "US", "zip_code": "94303", "phone": "+1 650 555 0100", "configuration": { "default_domain_redirect_url": "https://piedpiper.com", "default_email_forward_address": "forward@piedpiper.com" } } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `email` | string | Owner email address | | `first_name` | string | Owner first name | | `last_name` | string | Owner last name | | `company_name` | string/null | Company name | | `address` | string | Owner address | | `city` | string | Owner city | | `state` | string | Owner state or region | | `country` | string | Owner country code | | `zip_code` | string | Owner postal code | | `phone` | string | Owner phone number | | `configuration` | object | Default domain settings | |   └─ `default_domain_redirect_url` | string/null | Default redirect URL | |   └─ `default_email_forward_address` | string/null | Default forwarding address | Invalid request or malformed request syntax ```json { "title": "Bad Request", "status": 400, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Authentication failed. Please review the [authentication guide](/docs/getting-started/authentication.mdx) ```json { "title": "Unauthorized", "status": 401, "detail": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### 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 | Returned when the request body is valid JSON but contains invalid or missing fields. ```json { "type": "validation_error", "message": "Invalid field(s)", "code": "invalid_fields", "request_id": "1b752aaa-b067-4145-8e22-939582dfeb2c", "fields": [ { "field": "city", "issue": "required", "value": null } ] } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `type` | string | Error type | | `message` | string | Error message | | `code` | string | Error code | | `request_id` | string or null | Request identifier when available | | `fields` | array | Fields that failed validation | |   └─ `field` | string | Field with a validation issue | |   └─ `issue` | string | Detailed description of the validation issue | |   └─ `value` | string/null | Rejected value, or `null` for a missing required field | Unexpected server error ```json { "title": "Internal Server Error", "status": 500, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | Service unavailable or communication error ```json { "title": "Service Unavailable", "status": 503, "details": "error details", "timestamp": "2026-05-06T12:00:00Z" } ``` #### Body schema | Field | Type | Description | |-------|------|-------------| | `title` | string | A short title describing the error | | `status` | integer | The HTTP status code | | `details` | string | A detailed message explaining the error | | `timestamp` | string | The timestamp when the error occurred | --- ## Agency APIs Woodpecker API provides a set of endpoints for Agency users to manage multiple client accounts within a single Woodpecker account. While the standard endpoints operate at the individual account level, agency-specific endpoints introduce additional functionality, including client account creation, a master API key, agency-wide blacklists, and reporting. These features enable agencies to configure, monitor, and adjust multiple accounts more efficiently. Both sets of endpoints work together, with standard endpoints handling account-specific tasks and agency-specific endpoints providing broader control. Using them in combination allows for more effective account management. ## Prerequisites Access to the agency endpoints is a part of the `API keys & integrations` and the `Agency` add-ons. You can check your access in the [add-ons section](https://app.woodpecker.co/panel#add-ons). This feature is also available to all trial users with activated add-ons. --- ## Authentication(Agency-api) Woodpecker API follows a unified authentication mechanism across all endpoints, but when managing multiple accounts, you need to consider the company context. Your API requests may operate at different levels: * HQ account (your main company account) * Client accounts (individual accounts you manage) This guide covers general authentication and multi-account management using the HQ API key and the master API key. ## Generating an API key To authenticate requests, you must first generate an API key. [Click here](https://app.woodpecker.co/panel#add-ons/integrations/api-keys) to go to the API keys view of your HQ. You can also follow the instructions below; the process is the same for your HQ account and client accounts: 1. Log into the Woodpecker account where you want to generate an API key (either your HQ account or a client account) 2. Go to the Add-ons in the top-right corner → API & Integrations → 'API keys' 3. Click `Create a key` 4. You can add a label to each created key to describe what integration it is being used for API keys are user-specific, meaning each user only sees their own keys. Keep them private and do not share them with others. ## Authenticating requests The base URL is: ``` https://api.woodpecker.co/rest ``` All requests need to be authenticated using an `x-api-key` header. Try the request below, making sure to replace `{YOUR_API_KEY}` with your actual key. Note the difference in the response when using an **Agency HQ API key** versus a **client account API key** or **master API key**. ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v1/me" \ --header "x-api-key: {YOUR_API_KEY}" ``` ## Company context When working across multiple accounts, you have two ways to authenticate requests: 1. Using a **dedicated API key** for each account - generate a separate API key for each client, with the key operating only within given account. (Use [rest/v2/agency/companies/\{cid\}/api_keys](/docs/agency-api/companies/POST-companies-API-keys.mdx) to generate client keys via API) 1. Using the **HQ API key** with `x-company-id` header - send requests on behalf of different accounts without switching API keys. Include the `x-company-id` header to specify which account's data you are accessing. This is described further [below](#using-master-api-key). Use the HQ API key for managing multiple accounts. For endpoints that interact with specific accounts, either use a client's API key or specify x-company-id. :::tip Use the HQ API key for `/agency` endpoints. To access the Cold Email API for a specific client, choose one of the methods above. ::: ## Using master API key The HQ API key allows managing multiple accounts with a single key. To make use of this functionality, you need: * HQ API key - [how to generate it](#generating-an-api-key) * Client account ID (referred to as `company` in API terminology). You can retrieve it using [GET /companies](/docs/agency-api/companies/GET-companies.mdx) ### Example requests Lets consider an example where you want to retrieve mailboxes connected to company ID 123. You can: * Use an API key generated for this company ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/mailboxes" \ --header "x-api-key: API_KEY_COMPANY_123" ``` * Use the HQ API key together with the `x-api-key` header ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/mailboxes" \ --header "x-api-key: HQ_API_KEY" \ --header "x-company-id: 123" ``` * A similar request without the `x-api-key` header will return mailboxes connected to your HQ account ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/mailboxes" \ --header "x-api-key: HQ_API_KEY" ``` * You can also use an `/agency` endpoint that [retrieves email accounts for a specific company](/docs/agency-api/companies/GET-companies-email-accounts.mdx) ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/companies/123/email_accounts" \ --header "x-api-key: HQ_API_KEY" ``` ## Error codes `x-company-id` provided but not numeric ```json { "title": "Bad request", "status": 400, "detail": "X-Company-Id header value must be a number but is not", "timestamp": "2025-03-05 17:57:00" } ``` Please review whether you are using a correct API key, whether it is added to the header and whether your subscription grants API access. ```json { "title": "Unauthorized", "status": 401, "detail": "Invalid api key" | "No API addon" | "Upgrade your plan", "timestamp": "2025-03-05 17:57:00" } ``` `x-company-id` provided but it's not the key from the main agency account ``` Status: 403 Body: none ``` `x-company-id` provided but company does not exist within the agency from which the HQ key originates ```json { "title": "Not Found", "status": 404, "detail": "Company not found", "timestamp": "2025-03-05 17:57:00" } ``` `x-company-id` provided but company is inactive ```json { "title": "Conflict", "status": 409, "detail": "Company is inactive", "timestamp": "2025-03-05 17:57:00" } ``` Unexpected error, please try again later ```json { "title": "Internal Server Error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Blacklisting(Agency-api) Managing a do-not-contact list at the agency level is essential for maintaining a strong sender reputation across all client accounts. Agencies can blacklist both domains and individual email addresses, ensuring that all client accounts under the agency and the HQ account respect the same exclusion rules. This prevents unwanted outreach across multiple campaigns. ## Blacklisting emails Blacklisting an email address prevents any campaign from contacting that specific prospect across all client accounts and HQ of the agency. You can add, retrieve, and remove blacklisted emails via the API. ## Blacklisting domains You can also blacklist entire domains or domains matching a specific pattern. Use % as a wildcard, similar to the regex pattern .*: * `domain%` covers `domain-a.com`, `domain-b.org`, etc. * `%domain%.%` covers `getdomainnow.io`, `trydomainfree.com`, etc. --- ## Delete domains from the agency blacklist Remove specific domains from the agency blacklist. Removing a domain from the list doesn't change the status of a prospects if it was previously set to `BLACKLISTED` ## Request ### Endpoint ``` DELETE https://api.woodpecker.co/rest/v2/agency/blacklist/domains ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Body :::info You can remove up to 500 domains per request ::: ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } ``` | Field | Type | Description | | --------- | ------------- | --------------------------- | | `domains` | array[string] | List of domains to remove from blacklist | ### Request samples #### Remove a list of domains from blacklist ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/agency/blacklist/domains" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] }' ``` ```Python import requests def delete_blacklist_domains(): url = "https://api.woodpecker.co/rest/v2/agency/blacklist/domains" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } response = requests.delete(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"DELETE request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = delete_blacklist_domains() print("DELETE response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/blacklist/domains"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); String jsonPayload = "{\"domains\": [\"baddomain.com\", \"blacklistedomain.io\", \"nomoreemails.co\"]}"; HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("Content-Type", "application/json") .header("x-api-key", API_KEY) .method("DELETE", HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("DELETE response: " + response.body()); } else { System.err.println("DELETE request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function deleteBlacklistDomains() { const url = 'https://api.woodpecker.co/rest/v2/agency/blacklist/domains'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { domains: [ 'baddomain.com', 'blacklistedomain.io', 'nomoreemails.co' ] }; try { const response = await axios.delete(url, { headers, data }); if (response.status === 200) { console.log('DELETE response:', response.data); } else { console.error('DELETE request failed:', response.status); } } catch (error) { console.error('DELETE request failed:', error.response ? error.response.status : error.message); } } deleteBlacklistDomains(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->delete('agency/blacklist/domains', [ 'json' => [ 'domains' => [ 'baddomain.com', 'blacklistedomain.io', 'nomoreemails.co', ], ], ]); 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 Returns a list of successfully removed domains, including only those that were previously blacklisted; domains not found in the blacklist or with an invalid format are ignored and not included in the response. If none of the requested domains were blacklisted, the returned array will be empty. ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } ``` #### Body schema | Field | Type | Description | | --------- | ------------- | --------------------------- | | `domains` | array[string] | List of domains removed from the blacklist | Invalid request or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "You can proceed with up to 500 elements in one request" | "Domains parameter can not be empty" | "Value of domains is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Delete emails from the agency blacklist Remove specific emails from the agency blacklist. Removing an email from the list doesn't change the status of a prospects if it was previously set to `BLACKLISTED` ## Request ### Endpoint ``` DELETE https://api.woodpecker.co/rest/v2/agency/blacklist/emails ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Body :::info You can remove up to 500 emails per request ::: ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } ``` | Field | Type | Description | | -------- | ------------- | -------------------------- | | `emails` | array[string] | List of emails to remove from blacklist | ### Request samples #### Remove a list of emails from blacklist ```bash curl --request DELETE \ --url "https://api.woodpecker.co/rest/v2/agency/blacklist/emails" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] }' ``` ```Python import requests def delete_blacklist_emails(): url = "https://api.woodpecker.co/rest/v2/agency/blacklist/emails" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } response = requests.delete(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"DELETE request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = delete_blacklist_emails() print("DELETE response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/blacklist/emails"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); String jsonPayload = "{\"emails\": [\"wrong@baddomain.com\", \"worse@anotherone.com\", \"john@finisheddeal.co.uk\"]}"; HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("Content-Type", "application/json") .header("x-api-key", API_KEY) .method("DELETE", HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("DELETE response: " + response.body()); } else { System.err.println("DELETE request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function deleteBlacklistEmails() { const url = 'https://api.woodpecker.co/rest/v2/agency/blacklist/emails'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { emails: [ 'wrong@baddomain.com', 'worse@anotherone.com', 'john@finisheddeal.co.uk' ] }; try { const response = await axios.delete(url, { headers, data }); if (response.status === 200) { console.log('DELETE response:', response.data); } else { console.error('DELETE request failed:', response.status); } } catch (error) { console.error('DELETE request failed:', error.response ? error.response.status : error.message); } } deleteBlacklistEmails(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->delete('agency/blacklist/emails', [ 'json' => [ 'emails' => [ 'wrong@baddomain.com', 'worse@anotherone.com', 'john@finisheddeal.co.uk', ], ], ]); 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 Returns a list of successfully removed emails, including only those that were previously blacklisted; emails not found in the blacklist or with an invalid format are ignored and not included in the response. If none of the requested emails were blacklisted, the returned array will be empty. ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } ``` #### Body schema | Field | Type | Description | | --------- | ------------- | --------------------------- | | `emails` | array[string] | List of emails removed from the blacklist | Invalid request or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "You can proceed with up to 500 elements in one request" | "Emails parameter can not be empty" | "Value of emails is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Get blacklisted domains(Blacklisting) Retrieve a list of domains blacklisted in your agency. You can use the `domain_filter` parameter to check whether specific domains are included in the blacklist. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/agency/blacklist/domains ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Description | | --------------- | -------- | ------------------------------------------------------- | | `page` | No | Requested results page | | `per_page` | No | Number of records per page. Default: 100, maximum: 500 | | `domain_filter` | No | Comma-separated domains to check against the list. Use `*` as a wildcard - `woodpecker*` will match `woodpecker.co` and `woodpeckers.tld` | ### Request samples #### Retrieve first 500 domains ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/blacklist/domains?page=1&per_page=500" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_blacklist_domains(): url = "https://api.woodpecker.co/rest/v2/agency/blacklist/domains?page=1&per_page=500" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_blacklist_domains() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/blacklist/domains?page=1&per_page=500"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getBlacklistDomains() { const url = 'https://api.woodpecker.co/rest/v2/agency/blacklist/domains?page=1&per_page=500'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getBlacklistDomains(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->get('agency/blacklist/domains', [ 'query' => ['page' => 1, 'per_page' => 500], ]); echo $response->getStatusCode(), "\n"; echo $response->getBody(), "\n"; } catch (RequestException $e) { echo "Error: ", $e->getMessage(), "\n"; if ($e->hasResponse()) { echo $e->getResponse()->getBody(), "\n"; } } ``` #### Lookup a list of domains You can check whether certain domains are listed by using the `domain_filter` parameter. The maximum length of the request URL is 4100 characters. ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/blacklist/domains?domain_filter=baddomain.com,anotherone.com" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_blacklist_domains_by_filter(): url = "https://api.woodpecker.co/rest/v2/agency/blacklist/domains?domain_filter=baddomain.com,anotherone.com" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_blacklist_domains_by_filter() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/blacklist/domains?domain_filter=baddomain.com,anotherone.com"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getBlacklistDomainsByFilter() { const url = 'https://api.woodpecker.co/rest/v2/agency/blacklist/domains?domain_filter=baddomain.com,anotherone.com'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getBlacklistDomainsByFilter(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->get('agency/blacklist/domains', [ 'query' => ['domain_filter' => 'baddomain.com,anotherone.com'], ]); 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 A list of blacklisted domains ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "finisheddeal.co.uk", "nomoreemails.co", "notmyicp.design" ], "total": 5 } ``` ### Body schema | Field | Type | Description | | --------- | ------------- | ------------------------------------------------------------------------------------------------ | | `domains` | array[string] | List of blacklisted domains | | `total` | integer | Total number of blacklisted domains, or total number of found domains when using `domain_filter` | Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Value of {field_name} is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Get blacklisted emails(Blacklisting) Retrieve a list of emails blacklisted in your agency. You can use the `email_filter` parameter to check whether specific emails are included in the blacklist. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/agency/blacklist/emails ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Description | | ---------- | -------- | ------------------------------------------------------- | | `page` | No | Requested results page | | `per_page` | No | Number of records per page. Default: 100, maximum: 500 | | `email_filter` | No | Comma-separated emails to check against the list. Use `*` as a wildcard - `jimothy@woodpecker*` will match `jimothy@woodpecker.co` and `jimothy@woodpeckers.tld` | ### Request samples #### Retrieve first 500 emails ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/blacklist/emails?page=1&per_page=500" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_blacklist_emails(): url = "https://api.woodpecker.co/rest/v2/agency/blacklist/emails?page=1&per_page=500" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_blacklist_emails() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/blacklist/emails?page=1&per_page=500"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getBlacklistEmails() { const url = 'https://api.woodpecker.co/rest/v2/agency/blacklist/emails?page=1&per_page=500'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getBlacklistEmails(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->get('agency/blacklist/emails', [ 'query' => ['page' => 1, 'per_page' => 500], ]); echo $response->getStatusCode(), "\n"; echo $response->getBody(), "\n"; } catch (RequestException $e) { echo "Error: ", $e->getMessage(), "\n"; if ($e->hasResponse()) { echo $e->getResponse()->getBody(), "\n"; } } ``` #### Lookup a list of emails You can check whether certain emails are listed by using the `email_filter` parameter. The maximum length of the request URL is 4100 characters. ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/blacklist/emails?email_filter=wrong@baddomain.com,worse@anotherone.com" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_blacklist_emails_by_filter(): url = "https://api.woodpecker.co/rest/v2/agency/blacklist/emails?email_filter=wrong@baddomain.com,worse@anotherone.com" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_blacklist_emails_by_filter() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/blacklist/emails?email_filter=wrong@baddomain.com,worse@anotherone.com"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getBlacklistEmailsByFilter() { const url = 'https://api.woodpecker.co/rest/v2/agency/blacklist/emails?email_filter=wrong@baddomain.com,worse@anotherone.com'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getBlacklistEmailsByFilter(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->get('agency/blacklist/emails', [ 'query' => ['email_filter' => 'wrong@baddomain.com,worse@anotherone.com'], ]); 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 A list of blacklisted emails ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk", "drew@nomoreemails.co", "andrew@notmyicp.design" ], "total": 5 } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `emails` | array[string] | List of blacklisted emails | | `total` | integer | Total number of blacklisted emails, or total number of found emails when using `email_filter` | Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Value of {field_name} is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Blacklist domains(Blacklisting) Add domains to the agency blacklist. Blacklisting a domain does not immediately change the status of existing prospects. Instead, it prevents all current and future client accounts and the HQ from contacting prospects associated with that domain. A prospect's status will be updated to `BLACKLISTED` during campaign processing. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/agency/blacklist/domains ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Body :::info You can add up to 500 domains per request ::: ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } ``` | Field | Type | Description | | --------- | ------------- | --------------------------- | | `domains` | array[string] | List of domains to blacklist | ### Request samples #### Blacklist a list of domains ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/agency/blacklist/domains" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] }' ``` ```Python import requests def blacklist_domains(): url = "https://api.woodpecker.co/rest/v2/agency/blacklist/domains" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = blacklist_domains() print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/blacklist/domains"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); String jsonPayload = "{\"domains\": [\"baddomain.com\", \"blacklistedomain.io\", \"nomoreemails.co\"]}"; HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("Content-Type", "application/json") .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function blacklistDomains() { const url = 'https://api.woodpecker.co/rest/v2/agency/blacklist/domains'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { domains: [ 'baddomain.com', 'blacklistedomain.io', 'nomoreemails.co' ] }; 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); } } blacklistDomains(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->post('agency/blacklist/domains', [ 'json' => [ 'domains' => [ 'baddomain.com', 'blacklistedomain.io', 'nomoreemails.co', ], ], ]); 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 Returns a list of successfully blacklisted domains, including those newly added and those already blacklisted. ```json { "domains": [ "baddomain.com", "blacklistedomain.io", "nomoreemails.co" ] } ``` #### Body schema | Field | Type | Description | | --------- | ------------- | --------------------------- | | `domains` | array[string] | List of blacklisted domains | Invalid request body or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Domains parameter can not be empty" | "You can proceed with up to 500 elements in one request" | "All of passed domains were invalid" | "Value of domains is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Blacklist emails(Blacklisting) Add emails to the agency blacklist. Blacklisting an email does not immediately change the status of existing prospects. Instead, it prevents all current and future client accounts and the HQ from contacting that prospect. A prospect's status will be updated to `BLACKLISTED` during campaign processing. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/agency/blacklist/emails ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Body :::info You can add up to 500 emails per request ::: ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } ``` | Field | Type | Description | | ------------- | ------ | --------------------------- | | `emails` | array[string] | List of emails to blacklist | ### Request samples #### Blacklist a list of emails ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/agency/blacklist/emails" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] }' ``` ```Python import requests def blacklist_emails(): url = "https://api.woodpecker.co/rest/v2/agency/blacklist/emails" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } payload = { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = blacklist_emails() print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/blacklist/emails"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); String jsonPayload = "{\"emails\": [\"wrong@baddomain.com\", \"worse@anotherone.com\", \"john@finisheddeal.co.uk\"]}"; HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("Content-Type", "application/json") .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function blacklistEmails() { const url = 'https://api.woodpecker.co/rest/v2/agency/blacklist/emails'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { emails: [ 'wrong@baddomain.com', 'worse@anotherone.com', 'john@finisheddeal.co.uk' ] }; try { const response = await axios.post(url, data, { headers }); if (response.status === 200) { console.log('POST response:', response.data); } else { console.error('POST request failed:', response.status); } } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } blacklistEmails(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->post('agency/blacklist/emails', [ 'json' => [ 'emails' => [ 'wrong@baddomain.com', 'worse@anotherone.com', 'john@finisheddeal.co.uk', ], ], ]); 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 Returns a list of successfully blacklisted emails, including those newly added and those already blacklisted. ```json { "emails": [ "wrong@baddomain.com", "worse@anotherone.com", "john@finisheddeal.co.uk" ] } ``` #### Body schema | Field | Type | Description | | --------- | ------------- | --------------------------- | | `emails` | array[string] | List of blacklisted emails | Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Emails parameter can not be empty" | "You can proceed with up to 500 elements in one request" | "All of passed emails were invalid" | "Value of emails is incorrect.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Get a company's API keys Retrieve a paginated list of API keys associated with a specific company. The response will include only the keys created by the requesting user. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/api_keys ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Type | Description | | ---------- | -------- | ---- | ------------------------------------------------------- | | `company_id` | Yes | integer | Path parameter - the ID of the company for which the API keys will be returned | | `page` | No | integer | Requested results page (1-based) | ### Request samples #### Get list of API keys ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/api_keys" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_api_keys(company_id): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/api_keys" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID data = get_api_keys(company_id) print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID getApiKeys(companyId); } public static void getApiKeys(int companyId) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/api_keys"; java.net.URL obj = new java.net.URL(url); java.net.HttpURLConnection con = (java.net.HttpURLConnection) obj.openConnection(); con.setRequestMethod("GET"); con.setRequestProperty("x-api-key", API_KEY); int responseCode = con.getResponseCode(); System.out.println("GET Response Code : " + responseCode); if (responseCode == 200) { java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(con.getInputStream())); String inputLine; StringBuilder response = new StringBuilder(); while ((inputLine = in.readLine()) != null) { response.append(inputLine); } in.close(); System.out.println("GET response: " + response.toString()); } else { System.err.println("GET request failed: " + responseCode); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getApiKeys(companyId) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/api_keys`; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); // expected response 200 console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID await getApiKeys(companyId); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); $companyId = '{COMPANY_ID}'; try { $response = $client->get("agency/companies/{$companyId}/api_keys"); 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 A list of API keys. If there no API keys, `content` will be an empty array. ```json { "content": [ { "api_key": "123456.abcdefg123456hijk987", "label": "Custom name" }, { "api_key": "123456.123456abcd987efghijk", "label": "Custom name 2" } ], "pagination_data": { "total_elements": 2, "total_pages": 1, "current_page_number": 1, "page_size": 50 } } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `content` | array[object] | Array of API key objects | | └─`[].api_key` | string | API key | | └─`[].label ` | string/null | A descriptive name assigned while generating the key | | `pagination_data` | object | Pagination information. See the [pagination section](#pagination) | Invalid request parameters or malformed request syntax. ```json { "title": "Bad request", "status": 400, "detail": "string", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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 company does not exists, or the request URL is incorrect ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ### Pagination The response body contains pagination details. It will support you in navigating through larger datasets. Use `page` parameter to view a specific page. :::info Each page contains up to 50 API keys ::: ```json { "content": [], "pagination_data": { "total_elements": 300, "total_pages": 6, "current_page_number": 2, "page_size": 50 } } ``` | Field | Type | Description | | ----------------------- | ------- | ----------------------------------- | | `pagination_data` | object | Pagination information | | └─`total_elements` | integer | Total number of API keys | | └─`total_pages` | integer | Total number of available pages | | └─`current_page_number` | integer | Current page number (1-based) | | └─`page_size` | integer | Maximum number of items per page | --- ## Get list of email accounts assigned to the client This endpoint returns list of the email accounts linked to the client. By using this feature you can `GET` information including the type of email account, provider, and additional information such as error status, from name, IMAP ID, and the number of running campaigns associated with each account. For further actions, such as adding, refer to the related [/companies endpoints](companies.md). ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/email_accounts/ ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Description | | ---------- | -------- | ------------------------------------------------------- | | `company_id`| Yes | Company ID. This parameter is required. | ### Request samples #### Get list of email accounts ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/email_accounts" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_email_accounts(company_id): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/email_accounts" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID data = get_email_accounts(company_id) print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID getEmailAccounts(companyId); } public static void getEmailAccounts(int companyId) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/email_accounts"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(url)) .header("x-api-key", API_KEY) .GET() .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getEmailAccounts(companyId) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/email_accounts`; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID await getEmailAccounts(companyId); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); $companyId = '{COMPANY_ID}'; try { $response = $client->get("agency/companies/{$companyId}/email_accounts"); 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 A list of email accounts used by a specific agency client. ```json { "content": [ { "id": 0, "type": "IMAP", "details": { "email": "example@email.com", "provider": "provider1", "error": "string", "from_name": "string", "imap_id": 0, "running_campaigns": 2 } }, { "id": 1, "type": "SMTP", "details": { "email": "example@email.com", "provider": "provider1", "error": "string", "from_name": "string", "imap_id": 0, "running_campaigns": 0 } } ] } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `content` | array[object] | List of email accounts | | └─`[].id` | integer| Email account ID | | └─`[].type` | string | Type of email account | | └─`[].details` | object | Object with email account details | |     └─`email` | string | Email account address | |     └─`provider` | string | Email account provider name | |     └─`error` | string | Error details | |     └─`from_name` | string | From name | |     └─`imap_id` | integer| IMAP ID | |     └─`running_campaigns`| integer| Number of running campaigns | Invalid request parameters or malformed request syntax. ```json { "title": "Bad request", "status": 400, "detail": "string", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Get a list of company users Retrieve details of users under a specific company, including their ID, name, email, roles, and guest permissions. ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/users ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Type | Description | | ---------- | -------- | ---- | ------------------------------------------------------- | | `company_id`| Yes | integer | Path parameter - the ID of the company for which the users will be returned | | `page` | No | integer | Requested results page (1-based) | | `role` | No | string | Filter by user role: `admin`, `owner`, `guest`, `authorized_team_member` | ### Request samples #### Get a list of company users ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/users" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_users(company_id): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/users" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID data = get_users(company_id) print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID getUsers(companyId); } public static void getUsers(int companyId) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/users"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(url)) .header("x-api-key", API_KEY) .GET() .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getUsers(companyId) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/users`; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID await getUsers(companyId); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); $companyId = '{COMPANY_ID}'; try { $response = $client->get("agency/companies/{$companyId}/users"); 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 A list of company users ```json { "content": [ { "id": 1234, "name": "Michael Scott", "email": "michael@dundermifflin.com", "roles": ["admin"], "guest_permissions": [] }, { "id": 1235, "name": "Jim Halpert", "email": "jimothy@dundermifflin.com", "roles": ["admin", "owner"], "guest_permissions": [] }, { "id": 1236, "name": "Bob Vance", "email": "bob.vance@vancerefrigeration.com", "roles": ["guest"], "guest_permissions": ["mailboxes"] }, { "id": 1237, "name": "Pam Beesly", "email": "pam@dundermifflin.com", "roles": ["authorized_team_member"], "guest_permissions": [] } ], "pagination_data": { "total_elements": 4, "total_pages": 1, "current_page_number": 1, "page_size": 50 } } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `content` | array[object] | List of all users with access to the specific company | | └─`[].id` | integer| Unique user ID | | └─`[].name` | string | User's full name | | └─`[].email` | string | User's email | | └─`[].roles` | array[string] | List of roles assigned to a user. More about roles [here](https://woodpecker.co/help-center/en/articles/6871715) `admin` - set of permission that may be applied on the agency-level. Has full access to all companies `owner` - has the same permissions as an admin but only within a given company `authorized_team_member` - regular agency team member that has access to the company `guest` - can view campaign results, export data, and manage their email accounts | | └─`guest_permissions` | array[string] | `mailboxes` - whether a guest user can add/remove their mailboxes. Empty array for non-guests or guest without permission | | `pagination_data` | object | Pagination information. See the [pagination section](#pagination) | Invalid request parameters or malformed request syntax. ```json { "title": "Bad request", "status": 400, "detail": "string", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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 company does not exists, or the request URL is incorrect ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ### Pagination The response body contains pagination details. It will support you in navigating through larger datasets. Use `page` parameter to view a specific page. :::info Each page contains up to 50 users ::: ```json { "content": [], "pagination_data": { "total_elements": 501, "total_pages": 11, "current_page_number": 3, "page_size": 50 } } ``` | Field | Type | Description | | ----------------------- | ------- | ----------------------------------- | | `pagination_data` | object | Pagination information | | └─`total_elements` | integer | Total number of users | | └─`total_pages` | integer | Total number of available pages | | └─`current_page_number` | integer | Current page number (1-based) | | └─`page_size` | integer | Maximum number of items per page | --- ## Get companies Retrieve a list of companies managed by your agency, including their ID, name, owner's name, status (active or inactive), number of running campaigns, and number of connected accounts. Use this endpoint to get company IDs for other requests. Refer to the related [/companies endpoints](companies.md). ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/agency/companies ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Type | Description | | ---------- | -------- | ---- | ------------------------------------------------------- | | `active` | No | boolean | Whether to return only active or inactive comapnies | | `page` | No | integer | Requested results page (1-based) | ### Request samples #### Retrieve first page of companies ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/companies" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_companies(): url = "https://api.woodpecker.co/rest/v2/agency/companies" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_companies() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/companies"; public static void main(String[] args) { try { java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(URL)) .header("x-api-key", API_KEY) .GET() .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getCompanies() { const url = 'https://api.woodpecker.co/rest/v2/agency/companies'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } (async () => { await getCompanies(); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->get('agency/companies'); echo $response->getStatusCode(), "\n"; echo $response->getBody(), "\n"; } catch (RequestException $e) { echo "Error: ", $e->getMessage(), "\n"; if ($e->hasResponse()) { echo $e->getResponse()->getBody(), "\n"; } } ``` #### Retrieve a second page of active companies ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/companies?active=true&page=2" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_companies(): url = "https://api.woodpecker.co/rest/v2/agency/companies?active=true&page=2" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_companies() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/companies?active=true&page=2"; public static void main(String[] args) { try { java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(URL)) .header("x-api-key", API_KEY) .GET() .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getCompanies() { const url = 'https://api.woodpecker.co/rest/v2/agency/companies?active=true&page=2'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } (async () => { await getCompanies(); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->get('agency/companies', [ 'query' => [ 'active' => 'true', 'page' => 2, ], ]); 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 A list of clients companies. If there are no clients companies, `content` will be an empty array. ```json { "content": [ { "id": 12345678, "name": "Dunmore High School", "owner": "Jim Halpert", "active": true, "running_campaigns": 2, "email_slots": 2, "linkedin_slots": 1 }, { "id": 12345679, "name": "Beets Beets Beets", "owner": "Dwight Schrute", "active": false, "running_campaigns": 0, "email_slots": 0, "linkedin_slots": 0 } ], "pagination_data": { "total_elements": 2, "total_pages": 1, "current_page_number": 1, "page_size": 50 } } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `content` | array[object] | Array of company objects | | └─`[].id` | integer | Company ID | | └─`[].name` | string | Company name | | └─`[].owner` | string | Owner of the company, one of the team members | | └─`[].active` | boolean | Whether this company is active or not | | └─`[].running_campaigns` | integer | Number of campaigns running for that specific company | | └─`[].email_slots` | integer | Number of connected emails accounts | | └─`[].linkedin_slots` | integer | Number of connected LinkedIn accounts | | `pagination_data` | object | Pagination information. See the [pagination section](#pagination) | Invalid request parameters or malformed request syntax. ```json { "title": "Bad request", "status": 400, "detail": "Page parameter must be a positive number", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "title": "Not Found", "status": 404, "detail": "Requested resource does not exist", "timestamp": "2024-11-05 17:55:02" } ``` #### 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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "timestamp": "2024-11-05 17:55:02" } ``` #### 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 | ### Pagination The response body contains pagination details. It will support you in navigating through larger datasets. Use `page` parameter to view a specific page. :::info Each page contains up to 50 companies ::: ```json { "content": [], "pagination_data": { "total_elements": 300, "total_pages": 6, "current_page_number": 2, "page_size": 50 } } ``` | Field | Type | Description | | ----------------------- | ------- | ----------------------------------- | | `pagination_data` | object | Pagination information | | └─`total_elements` | integer | Total number of companies | | └─`total_pages` | integer | Total number of available pages | | └─`current_page_number` | integer | Current page number (1-based) | | └─`page_size` | integer | Maximum number of items per page | --- ## Create an API key for a company This endpoint allows you to generate a new API key for the specified company, which can be used to authenticate API requests. The response contains the newly created API key. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/api_keys ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Type | Description | | ---------- | -------- | ---- | ------------------------------------------------------- | | `company_id` | Yes | integer | Path parameter - the ID of the company for which the API key will be generated | ### Body ```json { "label": "string" } ``` #### Body schema | Field | Required | Type | Description | | ---------- | ---- | ----- | ------------------------------------------------------- | | `label` | Yes | string | A descriptive name assigned to the key | ### Request samples #### Create API keys ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/api_keys" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "label": "string" }' ``` ```Python import requests def create_api_key(company_id, label): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/api_keys" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } data = { "label": label } response = requests.post(url, headers=headers, json=data) if response.status_code == 201: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID label = "string" data = create_api_key(company_id, label) print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID String label = "string"; createApiKey(companyId, label); } public static void createApiKey(int companyId, String label) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/api_keys"; String jsonData = "{ \"label\": \"" + label + "\" }"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.ofString(jsonData)) .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 201) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function createApiKey(companyId, label) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/api_keys`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { label: label }; try { const response = await axios.post(url, data, { headers }); if (response.status === 201) { console.log('POST response:', response.data); } else { console.error('POST request failed:', response.status); } } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID const label = "string"; await createApiKey(companyId, label); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); $companyId = '{COMPANY_ID}'; try { $response = $client->post("agency/companies/{$companyId}/api_keys", [ 'json' => [ 'label' => 'string', ], ]); 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 Created API key ```json { "api_key": "string" } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `api_key` | string | newly generated API key | Missing request body: ``` Status: 400 Body: None ``` Malformed request syntax: ```json { "title": "Bad request", "status": 400, "detail": "Your request was not valid. Please check the body for any mistakes", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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 company does not exists, is inactive, or the request URL is incorrect ```json { "title": "Not Found", "status": 404, "detail": "No active company found" | "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Delete client's prospects Remove prospects from a specific company within your agency account. This endpoint allows you to `DELETE` prospects from the company, updating the company's prospect database accordingly. It provides a response with the deletion request ID and the number of prospects removed. This functionality is essential for managing the prospects associated with a company and ensuring accurate records. For further actions, such as adding, refer to the related [/companies endpoints](companies.md). ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/prospects/delete ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Description | | ---------- | -------- | ------------------------------------------------------- | | `company_id`| Yes | Company ID | ### Body The request body is a JSON object with the property `type' holding types of prospects to delete within the company ID. ```json { "type": "ALL" } ``` :::info Request body is required ::: :::warning This action is irreversible! ::: ### Request samples #### Remove prospects from the specific client's account ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/prospects/delete" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "type": "ALL" }' ``` ```Python import requests def delete_prospects(company_id): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/prospects/delete" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } data = { "type": "ALL" } response = requests.post(url, headers=headers, json=data) if response.status_code == 202: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID data = delete_prospects(company_id) print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID deleteProspects(companyId); } public static void deleteProspects(int companyId) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/prospects/delete"; String jsonData = "{ \"type\": \"ALL\" }"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.ofString(jsonData)) .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 202) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function deleteProspects(companyId) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/prospects/delete`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { type: "ALL" }; try { const response = await axios.post(url, data, { headers }); if (response.status === 202) { console.log('POST response:', response.data); } else { console.error('POST request failed:', response.status); } } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID await deleteProspects(companyId); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $companyId = '{company_id}'; try { $response = $client->post("agency/companies/{$companyId}/prospects/delete", [ 'json' => [ 'type' => 'ALL', ], ]); 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 ```json { "deletion_request_id": 0, "count": 0 } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `deletion_request_id` | integer | Request ID | | `count` | integer | Number of removed prospects | Invalid request parameters or malformed request syntax. ```json { "title": "Bad request", "status": 400, "detail": "string", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Invite guests to company This endpoint allows you to invite new guests to a company or resend invitations to those who haven't accepted yet. To modify permissions for already invited guests, use the [/guests/permissions endpoint](PUT-companies-guests-permissions.mdx). * If the guest hasn't been invited before, an invitation is sent, * If the guest was previously invited but hasn't accepted, the original invitation is resent (ignoring any new name or permissions), * If the guest has accepted the invitation, no new invitation is sent, but the initial invitation ID is returned, and no error is thrown. ## Request The request follows an all-or-none rule - if any guest object in the request is invalid (e.g., malformed email address), none will be invited. ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/invite_guest ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Type | Description | | ---------- | -------- | ---- | ------------------------------------------------------- | | `company_id` | Yes | integer | Path parameter - the ID of the company to which a guest will be invited | ### Body ```json { "guests": [ { "name": "Michael Scott", "email": "michael@dundermifflin.com", "guest_permissions": [] }, { "name": "Jimothy Halpert", "email": "jimothy@dundermifflin.com", "guest_permissions": ["mailboxes"] } ] } ``` #### Body schema | Field | Type | Required | Description | |------------|----------|--------|--------------------------------------| | `guests` | array[object] | Yes | A list of guests to invite | | └─`[].name` | string | Yes | Full name of the invited guest | | └─`[].email` | string | Yes | Guest's email address. It will be used as their login | | └─`[].guest_permissions` | array[string]/null | No | `mailboxes` - allows guests to add/remove their email accounts | :::info You can invite up to 10 guests with one request ::: ### Request samples #### Invite guests ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/invite_guest" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "guests": [ { "name": "Michael Scott", "email": "michael@dundermifflin.com", "guest_permissions": [] }, { "name": "Jimothy Halpert", "email": "jimothy@dundermifflin.com", "guest_permissions": ["mailboxes"] } ] }' ``` ```Python import requests def invite_guest(company_id, guests): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/invite_guest" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } data = { "guests": guests } response = requests.post(url, headers=headers, json=data) if response.status_code == 200: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID guests = [ { "name": "Michael Scott", "email": "michael@dundermifflin.com", "guest_permissions": [] }, { "name": "Jimothy Halpert", "email": "jimothy@dundermifflin.com", "guest_permissions": ["mailboxes"] } ] data = invite_guest(company_id, guests) print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID inviteGuest(companyId); } public static void inviteGuest(int companyId) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/invite_guest"; String jsonData = "{" + "\"guests\": [" + "{" + "\"name\": \"Michael Scott\"," + "\"email\": \"michael@dundermifflin.com\"," + "\"guest_permissions\": []" + "}," + "{" + "\"name\": \"Jimothy Halpert\"," + "\"email\": \"jimothy@dundermifflin.com\"," + "\"guest_permissions\": [\"mailboxes\"]" + "}" + "]" + "}"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.ofString(jsonData)) .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function inviteGuest(companyId, guests) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/invite_guest`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { guests: guests }; try { const response = await axios.post(url, data, { headers }); if (response.status === 200) { console.log('POST response:', response.data); } else { console.error('POST request failed:', response.status); } } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID const guests = [ { name: "Michael Scott", email: "michael@dundermifflin.com", guest_permissions: [] }, { name: "Jimothy Halpert", email: "jimothy@dundermifflin.com", guest_permissions: ["mailboxes"] } ]; await inviteGuest(companyId, guests); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $companyId = '{company_id}'; try { $response = $client->post("agency/companies/{$companyId}/invite_guest", [ 'json' => [ 'guests' => [ [ 'name' => 'Michael Scott', 'email' => 'michael@dundermifflin.com', 'guest_permissions' => [], ], [ 'name' => 'Jimothy Halpert', 'email' => 'jimothy@dundermifflin.com', 'guest_permissions' => ['mailboxes'], ], ], ], ]); 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 A list of invited guests ```json { "guests": [ { "invitation_id": 45678, "name": "Michael Scott", "email": "michael@dundermifflin.com", "guest_permissions": [] }, { "invitation_id": 45679, "name": "Jimothy Halpert", "email": "jimothy@dundermifflin.com", "guest_permissions": ["mailboxes"] } ] } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `guests` | array[object] | List of invited guests | | └─`[].invitation_id` | integer | A unique invitation ID, distinct from the user ID. If a guest has already been invited, the ID of the initial invitation is returned. If the guest hasn't accepted the invite yet, it will be resent | | └─`[].name` | string | Full name of the invited guest | | └─`[].email` | string | Guest's email address. It will be used as their login | | └─`[].guest_permissions`| array[string] | List of guest permissions. Empty array if no permissions were granted | Malformed request syntax ```json { "title": "Bad request", "status": 400, "detail": "Guest name must not be blank." | "Value of guests is incorrect." | "Guest email must not be blank." | "You must invite at least one guest." | "Value of guests is incorrect. Value of guest_permissions is incorrect." | "Guest email 'email.com' is not valid email address.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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 company does not exists, is inactive, or the request URL is incorrect ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Create companies Create new client accounts under your agency account, each with its own prospect database, campaigns, and team access. Each company created via API will automatically have an API key generated. Use [/companies/\{company_id\}/api_keys](GET-companies-API-keys.mdx) to fetch it. ## Request The user making the request will automatically become the owner of the company. The request follows an all-or-none rule - if any company in the request is invalid (e.g., the name is already taken), none will be created. :::warning Adding a new client account comes with a fee, and you'll be charged periodically for each active account. See the [pricing page](https://woodpecker.co/pricing/) for more details. ::: ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/agency/companies ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Body ```json { "companies": [ { "name": "My first client" }, { "name": "My second client" } ] } ``` :::info You can add up to 10 companies in one request ::: #### Body schema | Field | Type | Required | Description | |------------|----------|--------|--------------------------------------| | `companies` | array[object] | Yes | A list of companies to be created | | └─`companies[].name` | string | Yes | Name of a company | ### Request samples #### Create new companies ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/agency/companies" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "companies": [ { "name": "My first client" }, { "name": "My second client" } ] }' ``` ```Python import requests def create_companies(companies): url = "https://api.woodpecker.co/rest/v2/agency/companies" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } data = { "companies": companies } response = requests.post(url, headers=headers, json=data) if response.status_code == 201: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: companies = [ {"name": "My first client"}, {"name": "My second client"} ] data = create_companies(companies) print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/companies"; public static void main(String[] args) { createCompanies(); } public static void createCompanies() { try { String jsonData = "{" + "\"companies\": [" + "{ \"name\": \"My first client\" }," + "{ \"name\": \"My second client\" }" + "]" + "}"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(URL)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.ofString(jsonData)) .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 201) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function createCompanies(companies) { const url = 'https://api.woodpecker.co/rest/v2/agency/companies'; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { companies: companies }; try { const response = await axios.post(url, data, { headers }); if (response.status === 201) { console.log('POST response:', response.data); } else { console.error('POST request failed:', response.status); } } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } (async () => { const companies = [ { name: "My first client" }, { name: "My second client" } ]; await createCompanies(companies); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('agency/companies', [ 'json' => [ 'companies' => [ ['name' => 'My first client'], ['name' => 'My second client'], ], ], ]); 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 A list of created companies ```json { "companies": [ { "id": 98764, "name": "My first client", "owner": "Michael Scott", "active": true }, { "id": 98765, "name": "My second client", "owner": "Michael Scott", "active": true } ] } ``` ### Body schema | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------ | | `companies` | array[object] | An array of created companies | | └─`id` | integer | Company ID | | └─`name` | string | Company name | | └─`owner` | string | Owner of the company, the user making the request | | └─`active` | boolean | Indicates whether this company is active or not. When creating a company always `true` | Invalid request parameters or malformed request syntax. ```json { "title": "Bad request", "status": 400, "detail": "Value of companies is incorrect." | "Requested company name must not be blank." | "Requested company names are not unique. Found duplicate: duplicated name" | "You can create up to 10 companies with one request.", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Please review the request URL. ```json { "title": "Unprocessable Entity", "status": 422, "detail": "Company with given name already exist in agency: company name", "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Update guest permissions This feature allows you to modify the permissions associated with a particular guest, such as granting access to certain areas or resources. For further actions, such as inviting guests, refer to the related [/invite_guest endpoint](POST-companies-invite-guest.mdx). ## Request ### Endpoint ``` PUT https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/guests/permissions/ ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Body ```json { "guests": [ { "id": 1234, "permissions": [] }, { "id": 2345, "permissions": ["mailboxes"] } ] } ``` :::info Request body is required ::: ### Request samples #### Update permissions ```bash curl --request PUT \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/guests/permissions" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "guests": [ { "id": 1234, "permissions": [] }, { "id": 2345, "permissions": ["mailboxes"] } ] }' ``` ```Python import requests def update_guest_permissions(company_id, guests): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/guests/permissions" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } data = { "guests": guests } response = requests.put(url, headers=headers, json=data) if response.status_code == 200: return response.json() else: raise Exception(f"PUT request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID guests = [ {"id": 1234, "permissions": []}, {"id": 2345, "permissions": ["mailboxes"]} ] data = update_guest_permissions(company_id, guests) print("PUT response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID updateGuestPermissions(companyId); } public static void updateGuestPermissions(int companyId) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/guests/permissions"; String jsonData = "{" + "\"guests\": [" + " { \"id\": 1234, \"permissions\": [] }," + " { \"id\": 2345, \"permissions\": [\"mailboxes\"] }" + "]" + "}"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .PUT(java.net.http.HttpRequest.BodyPublishers.ofString(jsonData)) .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("PUT response: " + response.body()); } else { System.err.println("PUT request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function updateGuestPermissions(companyId, guests) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/guests/permissions`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { guests: guests }; try { const response = await axios.put(url, data, { headers }); if (response.status === 200) { console.log('PUT response:', response.data); } else { console.error('PUT request failed:', response.status); } } catch (error) { console.error('PUT request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID const guests = [ { id: 1234, permissions: [] }, { id: 2345, permissions: ["mailboxes"] } ]; await updateGuestPermissions(companyId, guests); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); $companyId = '{company_id}'; try { $response = $client->put("agency/companies/{$companyId}/guests/permissions", [ 'json' => [ 'guests' => [ [ 'id' => 1234, 'permissions' => [], ], [ 'id' => 2345, 'permissions' => ['mailboxes'], ], ], ], ]); 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 ``` Status: 200 Body: none ``` Invalid request parameters or malformed request syntax. ```json { "title": "Bad request", "status": 400, "detail": "string", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Companies The `/companies` endpoint allows you to manage the companies under your agency account. You can easily get the list of all companies you provide services for and check the basic information about them. You can also add new clients to your account, invite guests to the created companies or resend the invitation. You have also possibility to create an API key for each company and receive list of API keys for them. You can find out more about this feature in our [help center article](https://woodpecker.co/help-center/en/articles/6871219-agency-panel-client-panel). --- ## Get deliverability statistics #### Retrieve quantitative data on your clients' campaign performance. This endpoint provides key campaign metrics, including the total number of emails sent, delivered, opened, replied to, and bounced within a specified period. Additionally, it includes a comparison with the previous period of the same duration. The returned numbers represent the total counts of specific actions for each client account. For example, `emails_sent` reflects the total number of emails sent across multiple steps in campaigns, not the number of unique prospects contacted. If a company has had no activity within the specified period, it will not be included in the response payload. The data returned by the API represents the data available in the Agency Panel under the [deliverability tab](https://agency.woodpecker.co/deliverability). ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/agency/deliverability ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters By default the API returns results for up to 20 companies, for the last 30 days. You can modify the date range and the quantity of returned companies by using the below parameters. | Parameter | Required | Description | | ------------- | -------- | ------------------------------------------------------ | | `from` | No | the start date (YYYY-MM-DD) for the report, based on the user's timezone | | `to` | No | the end date (YYYY-MM-DD) for the report, based on the user's timezone | | `page` | No | Requested results page (1-based) | | `per_page` | No | Number of companies per page. Default: 20, maximum: 50 | ### Request samples #### Fetch deliverability data for the last 30 days ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/agency/deliverability" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```Python import requests def get_deliverability(): url = "https://api.woodpecker.co/rest/v2/agency/deliverability" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_deliverability() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/agency/deliverability"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getDeliverability() { const url = 'https://api.woodpecker.co/rest/v2/agency/deliverability'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getDeliverability(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); try { $response = $client->get('agency/deliverability'); 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 ```json { "content": [ { "id": 123, "name": "First client company", "emails_sent": { "current_period": 1016, "previous_period": 462 }, "delivered_emails": { "current_period": 998, "previous_period": 438 }, "views": { "current_period": 712, "previous_period": 303 }, "replies": { "current_period": 73, "previous_period": 34 }, "bounces": { "current_period": 18, "previous_period": 24 } }, { "id": 456, "name": "Second client company", "emails_sent": { "current_period": 1981, "previous_period": 1199 }, "delivered_emails": { "current_period": 1957, "previous_period": 1187 }, "views": { "current_period": 1063, "previous_period": 717 }, "replies": { "current_period": 113, "previous_period": 64 }, "bounces": { "current_period": 24, "previous_period": 12 } } ], "pagination_data": { "total_count": 22, "pages_count": 2, "current_page_number": 2, "page_size": 20 } } ``` #### Body schema | Field | Type | Description | |---------------------------|---------|-------------| | `content` | array[object] | List of client companies and their deliverability statistics | | └─`[].id` | Integer | Unique identifier for the client company | | └─`[].name` | String | Name of the client company | | └─`[].emails_sent` | Object | Object containing information about sent emails | |     └─`current_period` | Integer | Number of emails sent in the requested period | |     └─`previous_period` | Integer/null | Number of emails sent in the previous period | | └─`[].delivered_emails` | Object | Object containing information about delivered emails | |     └─`current_period` | Integer | Number of emails delivered in the requested period | |     └─`previous_period` | Integer/null | Number of emails delivered in the previous period | | └─`[].views` | Object | Object containing information about opened emails | |     └─`current_period` | Integer | Number of email views in the requested period | |     └─`previous_period` | Integer/null | Number of email views in the previous period | | └─`[].replies` | Object | Object containing information about replies | |     └─`current_period` | Integer | Number of replies in the requested period | |     └─`previous_period` | Integer/null | Number of replies in the previous period | | └─`[].bounces` | Object | Object containing information about bounced emails | |     └─`current_period` | Integer | Number of email bounces in the requested period | |     └─`previous_period` | Integer/null | Number of email bounces in the previous period | Invalid request parameters or malformed request syntax. ```json { "title": "Bad Request", "status": 400, "detail": "Value of from is incorrect" | "Value of to is incorrect" | "From date has to be later then 2021-01-01 UTC" | "From date has to be before to date", "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 | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | ### Pagination The response body contains pagination details. It will support you in navigating through larger datasets. Use the `per_page` [parameter](#parameters) to reduce or increase the number of returned domains per page. Default value: 20, maximum value: 50. Use `page` [parameter](#parameters) to view a specific page. ```json "pagination_data": { "total_elements": 501, "total_pages": 21, "current_page_number": 3, "page_size": 25 } ``` | Field | Type | Description | | ----------------------- | ------- | ----------------------------------- | | `pagination_data` | object | Pagination information | | └─`total_elements` | integer | Total number of returned companies | | └─`total_pages` | integer | Total number of available pages | | └─`current_page_number` | integer | Current page number (1-based) | | └─`page_size` | integer | Maximum number of items per page | --- ## Connect LinkedIn account Create a LinkedIn account within the company and returns the account ID together with an authorization URL. The user must open the URL to authenticate with LinkedIn and complete the connection. The connection link is valid for 48 hours. :::info To connect a LinkedIn account, you need an available LinkedIn slot. An account admin can manage slots in the billing section of the app. ::: ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/linkedin_accounts ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Type | Description | |--------------|----------|---------|-------------------------------------------------------------------------------| | `company_id` | Yes | integer | Path parameter - ID of the company where the LinkedIn account will be created | ### Body ```json { "timezone": "Europe/Warsaw" } ``` #### Body schema | Field | Required | Type | Description | | ---------- | ---- | ----- | ------------------------------------------------------- | | `timezone` | Yes | string | Canonical IANA time zone identifier representing the account's local time. (e.g. America/New_York, Europe/London).
List of accepted timezones Africa/Abidjan Africa/Algiers Africa/Bissau Africa/Cairo Africa/Casablanca Africa/El_Aaiun Africa/Johannesburg Africa/Khartoum Africa/Lagos Africa/Maputo Africa/Monrovia Africa/Nairobi Africa/Ndjamena Africa/Sao_Tome Africa/Tripoli Africa/Tunis Africa/Windhoek America/Anchorage America/Argentina/Buenos_Aires America/Asuncion America/Barbados America/Belize America/Bogota America/Caracas America/Cayenne America/Chicago America/Chihuahua America/Costa_Rica America/Denver America/Edmonton America/El_Salvador America/Grand_Turk America/Guatemala America/Guayaquil America/Guyana America/Halifax America/Havana America/Jamaica America/La_Paz America/Lima America/Los_Angeles America/Managua America/Manaus America/Martinique America/Mazatlan America/Mexico_City America/Miquelon America/Monterrey America/Montevideo America/New_York America/Panama America/Paramaribo America/Phoenix America/Port-au-Prince America/Puerto_Rico America/Regina America/Rio_Branco America/Santiago America/Santo_Domingo America/Sao_Paulo America/St_Johns America/Tegucigalpa America/Tijuana America/Toronto America/Vancouver America/Whitehorse America/Winnipeg Asia/Almaty Asia/Amman Asia/Ashgabat Asia/Baghdad Asia/Baku Asia/Bangkok Asia/Beirut Asia/Bishkek Asia/Colombo Asia/Damascus Asia/Dhaka Asia/Dubai Asia/Dushanbe Asia/Gaza Asia/Hong_Kong Asia/Irkutsk Asia/Jakarta Asia/Jerusalem Asia/Kabul Asia/Kamchatka Asia/Karachi Asia/Kathmandu Asia/Kolkata Asia/Krasnoyarsk Asia/Macau Asia/Magadan Asia/Manila Asia/Nicosia Asia/Novosibirsk Asia/Omsk Asia/Pyongyang Asia/Qatar Asia/Riyadh Asia/Seoul Asia/Shanghai Asia/Singapore Asia/Taipei Asia/Tashkent Asia/Tbilisi Asia/Tehran Asia/Thimphu Asia/Tokyo Asia/Ulaanbaatar Asia/Urumqi Asia/Vladivostok Asia/Yakutsk Asia/Yerevan Atlantic/Azores Atlantic/Bermuda Atlantic/Cape_Verde Atlantic/Faroe Atlantic/South_Georgia Atlantic/Stanley Australia/Adelaide Australia/Brisbane Australia/Darwin Australia/Hobart Australia/Melbourne Australia/Perth Australia/Sydney Europe/Andorra Europe/Athens Europe/Belgrade Europe/Berlin Europe/Brussels Europe/Bucharest Europe/Budapest Europe/Chisinau Europe/Dublin Europe/Gibraltar Europe/Helsinki Europe/Istanbul Europe/Lisbon Europe/London Europe/Madrid Europe/Malta Europe/Minsk Europe/Moscow Europe/Paris Europe/Prague Europe/Riga Europe/Rome Europe/Sofia Europe/Tallinn Europe/Tirane Europe/Vienna Europe/Vilnius Europe/Volgograd Europe/Warsaw Europe/Zurich Indian/Chagos Indian/Maldives Indian/Mauritius Pacific/Auckland Pacific/Efate Pacific/Fakaofo Pacific/Fiji Pacific/Guadalcanal Pacific/Guam Pacific/Honolulu Pacific/Kiritimati Pacific/Nauru Pacific/Niue Pacific/Norfolk Pacific/Noumea Pacific/Pago_Pago Pacific/Palau Pacific/Pitcairn Pacific/Port_Moresby Pacific/Rarotonga Pacific/Tahiti Pacific/Tarawa Pacific/Tongatapu
| ### Request samples ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/linkedin_accounts" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "timezone": "Europe/Warsaw" }' ``` ```Python import requests def connect_linkedin(company_id, timezone): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/linkedin_accounts" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } data = { "timezone": timezone } response = requests.post(url, headers=headers, json=data) if response.status_code == 201: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID timezone = "Europe/Warsaw" data = connect_linkedin(company_id, timezone) print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID String timezone = "Europe/Warsaw"; connectLinkedinAccount(companyId, timezone); } public static void connectLinkedinAccount(int companyId, String timezone) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/linkedin_accounts"; String jsonData = "{ \"timezone\": \"" + timezone + "\" }"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.ofString(jsonData)) .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 201) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function connectLinkedinAccount(companyId, timezone) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/linkedin_accounts`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; const data = { timezone: timezone }; try { const response = await axios.post(url, data, { headers }); if (response.status === 201) { console.log('POST response:', response.data); } else { console.error('POST request failed:', response.status); } } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID const timezone = "Europe/Warsaw"; await connectLinkedinAccount(companyId, timezone); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); $companyId = '{COMPANY_ID}'; try { $response = $client->post("agency/companies/{$companyId}/linkedin_accounts", [ 'json' => [ 'timezone' => 'Europe/Warsaw', ], ]); 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 Account created in Woodpecker, use the generated link to finish the authorization process. After successful authorization, the account becomes active and ready to use. ```json { "account_id": 123, "connect_account_link": "https://app.edges.run/identities/linkedin/login?token=ey6Ickv8p9.eys4JpZ3BGl0" } ``` ### Body schema | Field | Type | Description | |------------------------|---------|------------------------------------------------| | `account_id` | integer | ID of the newly created LinkedIn account | | `connect_account_link` | string | URL to use to finish the authorization process | Missing request body: ```json { "type": "VALIDATION_ERROR", "code": "BAD_REQUEST", "message": "Missing request body", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5" } ``` Malformed request: ```json { "type": "VALIDATION_ERROR", "code": "BAD_REQUEST", "message": "Invalid field(s).", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5", "fields": [ { "field": "timezone", "issue": "string" } ] } ``` #### Body schema | Field | Type | Description | |-----------------------|------------------------|--------------------------------------------------------------------------| | `type` | string | High-level category of the error | | `code` | string | Text representation of the HTTP status code | | `message` | string | Human-readable explanation of the problem | | `request_id` | string | Unique identifier of the request. Provide it when contacting support | | `fields` | Optional array[object] | Present when the error is related to specific request fields | |   └─`field` | string | Name of the field that failed validation | |   └─`issue` | string | Description of the validation issue for the field. `required`, `invalid` | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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 company does not exists, is inactive, or the request URL is incorrect ```json { "title": "Not Found", "status": 404, "detail": "No active company found" | "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 | There are no available LinkedIn slots in your account. Please contact your administrator. ```json { "type": "VALIDATION_ERROR", "code": "UNPROCESSABLE_ENTITY", "message": "No free LinkedIn slots available. Purchase more slots.", "request_id": "fcdd7349-2a71-43bf-b31d-ab5c15a8f833" } ``` #### Body schema | Field | Type | Description | |--------------|--------|----------------------------------------------------------------------| | `type` | string | High-level category of the error | | `code` | string | Text representation of the HTTP status code | | `message` | string | Human-readable explanation of the problem | | `request_id` | string | Unique identifier of the request. Provide it when contacting support | Unexpected error, please try again later ```json { "type": "INTERNAL_ERROR", "code": "INTERNAL_SERVER_ERROR", "message": "An unexpected error occurred on the server.", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5" } ``` #### Body schema | Field | Type | Description | |--------------|--------|----------------------------------------------------------------------| | `type` | string | High-level category of the error | | `code` | string | Text representation of the HTTP status code | | `message` | string | Human-readable description of what went wrong | | `request_id` | string | Unique identifier of the request. Provide it when contacting support | --- ## Generate connection link Generates a new authorization URL for an existing LinkedIn account. The user must open the link to authenticate with LinkedIn or reconnect the account. To track whether the account was connected or later disconnected without polling, subscribe to the [linkedin_automation_account_connected](/docs/webhooks/linkedin-account-connected.mdx) and [linkedin_automation_account_disconnected](/docs/webhooks/linkedin-account-disconnected.mdx) webhooks. The connection link is valid for 48 hours. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/linkedin_accounts/{account_id}/generate-connect-link ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-Type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/agency-api/authentication-agency.mdx). ### Parameters | Parameter | Required | Type | Description | |--------------|----------|---------|----------------------------------------------------------------------------------------------------| | `company_id` | Yes | integer | Path parameter - ID of the company in which the LinkedIn account exists | | `account_id` | Yes | integer | Path parameter - ID of the LinkedIn account for which the new authorization link will be generated | ### Body This endpoint does not require a request body. ### Request samples ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/linkedin_accounts/{account_id}/generate-connect-link" \ --header "x-api-key: {YOUR_API_KEY}" \ --header "Content-Type: application/json" ``` ```Python import requests def generate_connect_link(company_id, account_id): url = f"https://api.woodpecker.co/rest/v2/agency/companies/{company_id}/linkedin_accounts/{account_id}/generate-connect-link" headers = { "x-api-key": "{YOUR_API_KEY}", "Content-Type": "application/json" } response = requests.post(url, headers=headers) if response.status_code == 201: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: company_id = 123 # Example company ID account_id = 456 # Example LinkedIn account ID data = generate_connect_link(company_id, account_id) print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; public static void main(String[] args) { int companyId = 123; // Example company ID int accountId = 456; // Example LinkedIn account ID generateConnectLink(companyId, accountId); } public static void generateConnectLink(int companyId, int accountId) { try { String url = "https://api.woodpecker.co/rest/v2/agency/companies/" + companyId + "/linkedin_accounts/" + accountId + "/generate-connect-link"; java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() .uri(new java.net.URI(url)) .header("x-api-key", API_KEY) .header("Content-Type", "application/json") .POST(java.net.http.HttpRequest.BodyPublishers.noBody()) .build(); java.net.http.HttpResponse response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 201) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function generateConnectLink(companyId, accountId) { const url = `https://api.woodpecker.co/rest/v2/agency/companies/${companyId}/linkedin_accounts/${accountId}/generate-connect-link`; const headers = { 'x-api-key': '{YOUR_API_KEY}', 'Content-Type': 'application/json' }; try { const response = await axios.post(url, null, { headers }); if (response.status === 201) { console.log('POST response:', response.data); } else { console.error('POST request failed:', response.status); } } catch (error) { console.error('POST request failed:', error.response ? error.response.status : error.message); } } (async () => { const companyId = 123; // Example company ID const accountId = 456; // Example LinkedIn account ID await generateConnectLink(companyId, accountId); })(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => ['x-api-key' => getenv('WOODPECKER_API_KEY')], ]); $companyId = '{COMPANY_ID}'; $accountId = '{ACCOUNT_ID}'; try { $response = $client->post("agency/companies/{$companyId}/linkedin_accounts/{$accountId}/generate-connect-link"); 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 Connection link generated, use it to finish the authorization process. After successful authorization, the account becomes active and ready to use. ```json { "account_id": 123, "connect_account_link": "https://app.edges.run/identities/linkedin/login?token=ey6Ickv8p9.eys4JpZ3BGl0" } ``` ### Body schema | Field | Type | Description | |------------------------|---------|------------------------------------------------| | `account_id` | integer | ID of the LinkedIn account | | `connect_account_link` | string | URL to use to finish the authorization process | Non-existing or incorrect `account_id` ```json { "type": "VALIDATION_ERROR", "code": "BAD_REQUEST", "message": "Invalid account id.", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5" } ``` #### Body schema | Field | Type | Description | |--------------|--------|----------------------------------------------------------------------| | `type` | string | High-level category of the error | | `code` | string | Text representation of the HTTP status code | | `message` | string | Human-readable explanation of the problem | | `request_id` | string | Unique identifier of the request. Provide it when contacting support | An issue with authorization. Please review the [authorization guide](/docs/agency-api/authentication-agency.mdx) ```json { "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 company does not exists, is inactive, or the request URL is incorrect ```json { "title": "Not Found", "status": 404, "detail": "No active company found" | "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 | Unexpected error, please try again later ```json { "type": "INTERNAL_ERROR", "code": "INTERNAL_SERVER_ERROR", "message": "An unexpected error occurred on the server.", "request_id": "dc4faa0f-78bf-54e9-9d5d-ce9538f2eec5" } ``` #### Body schema | Field | Type | Description | |--------------|--------|----------------------------------------------------------------------| | `type` | string | High-level category of the error | | `code` | string | Text representation of the HTTP status code | | `message` | string | Human-readable description of what went wrong | | `request_id` | string | Unique identifier of the request. Provide it when contacting support | --- ## Connecting LinkedIn accounts The `/linkedin_accounts` endpoints allow you to create and reconnect LinkedIn accounts associated with companies within your agency. You can create a new LinkedIn account or obtain an authorization link that the user can open to grant access. If the account has been disconnected or the previous link expired, you can generate a new connection link for an existing account. To connect a LinkedIn account, **you need an available LinkedIn slot**. An account admin can manage slots in the billing section of the app. To check the current status of a LinkedIn account, use [GET linkedin_accounts](/docs/linkedin/get-linkedin-accounts.mdx) endpoint. Mind that this is an account-level endpoint, not an agency one --- ## Subscribe to a webhook :::warning This is a V1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to V2. While it remains functional, consider handling errors accordingly. ::: Start receiving notifications for selected events via webhook. Whenever an event occurs, we will send the event data as an array of objects to the specified target URL. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v1/webhooks/subscribe ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body You can subscribe to the same event up to five times per account, provided that each subscription uses a unique target_url. However, you can subscribe multiple different events to a single target_url. ```json { "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME" } ``` #### Body schema | Field | Type | Description | |---------|------|-------------| | `target_url` | string | The URL where webhook events will be delivered | | `event` | string | Event you would like to be notified about. [Available events](/docs/webhooks/webhooks.md#available-events) | ### Request samples #### Subscribe to a webhook ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v1/webhooks/subscribe" \ --header "Content-Type: application/json" \ --header "x-api-key: {YOUR_API_KEY}" \ --data '{ "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME" }' ``` ```Python import requests def subscribe_webhook(): url = "https://api.woodpecker.co/rest/v1/webhooks/subscribe" headers = { "Content-Type": "application/json", "x-api-key": "{YOUR_API_KEY}" } payload = { "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME" } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = subscribe_webhook() print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v1/webhooks/subscribe"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); String jsonPayload = "{\"target_url\": \"https://receiving-url.com/unique_target_url\", \"event\": \"EVENT_NAME\"}"; HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("Content-Type", "application/json") .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 201) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function subscribeWebhook() { const url = 'https://api.woodpecker.co/rest/v1/webhooks/subscribe'; const headers = { 'Content-Type': 'application/json', 'x-api-key': '{YOUR_API_KEY}' }; const data = { "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME" }; 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); } } subscribeWebhook(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('webhooks/subscribe', [ 'json' => [ 'target_url' => 'https://receiving-url.com/unique_target_url', 'event' => 'EVENT_NAME', ], ]); 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 Successfully subscribed ```json { "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME", "message": "Subscribed." } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|-----------------------------------------------------------| | `target_url`| string | The URL where webhook events will be delivered | | `event` | string | The name of the event for which the subscription was created | | `message` | string | A confirmation message indicating the subscription status | Invalid request or malformed request syntax. Please review the [request body](#body) ```json { "status": { "status": "ERROR", "code": "E_WRONG_PARAM", "msg": "Invalid target_url." | "Provided event is incorrect." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/campaign_list/someMadeUpURL" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Conflict. Either you are already subscribed for a given URL - event pair, or you have reached the event subscription limit ```json { "status": { "status": "ERROR", "code": "E_WRONG_PARAM", "msg": "Event defined for this url." | "You can only subscribe to the same webhook 5 times per one account." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Unsubscribe from a webhook :::warning This is a V1 legacy endpoint. It uses a different path `/rest/v1` and may return different error codes and [response formats](#response) compared to V2. While it remains functional, consider handling errors accordingly. ::: Use this endpoint to stop receiving notifications for specific events through a webhook. ## Request ### Endpoint ``` POST https://api.woodpecker.co/rest/v1/webhooks/unsubscribe ``` ### Headers ``` x-api-key: {YOUR_API_KEY} Content-type: application/json ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Body ```json { "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME" } ``` #### Body schema | Field | Type | Description | |---------|------|-------------| | `target_url` | string | The URL where webhook events are currently delivered | | `event` | string | Event you would like to unsubscribe from [Available events](/docs/webhooks/webhooks.md#available-events) | ### Request samples #### Subscribe to a webhook ```bash curl --request POST \ --url "https://api.woodpecker.co/rest/v1/webhooks/unsubscribe" \ --header "Content-Type: application/json" \ --header "x-api-key: {YOUR_API_KEY}" \ --data '{ "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME" }' ``` ```Python import requests def unsubscribe_webhook(): url = "https://api.woodpecker.co/rest/v1/webhooks/unsubscribe" headers = { "Content-Type": "application/json", "x-api-key": "{YOUR_API_KEY}" } payload = { "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME" } response = requests.post(url, headers=headers, json=payload) if response.status_code == 200: return response.json() else: raise Exception(f"POST request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = unsubscribe_webhook() print("POST response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v1/webhooks/unsubscribe"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); String jsonPayload = "{\"target_url\": \"https://receiving-url.com/unique_target_url\", \"event\": \"EVENT_NAME\"}"; HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("Content-Type", "application/json") .header("x-api-key", API_KEY) .POST(HttpRequest.BodyPublishers.ofString(jsonPayload)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("POST response: " + response.body()); } else { System.err.println("POST request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function unsubscribeWebhook() { const url = 'https://api.woodpecker.co/rest/v1/webhooks/unsubscribe'; const headers = { 'Content-Type': 'application/json', 'x-api-key': '{YOUR_API_KEY}' }; const data = { "target_url": "https://receiving-url.com/unique_target_url", "event": "EVENT_NAME" }; 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); } } unsubscribeWebhook(); ``` ```php 'https://api.woodpecker.co/rest/v1/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), 'Content-Type' => 'application/json', ], ]); try { $response = $client->post('webhooks/unsubscribe', [ 'json' => [ 'target_url' => 'https://receiving-url.com/unique_target_url', 'event' => 'EVENT_NAME', ], ]); 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 Successfully unsubscribed ```json Status: 200 Body: none ``` Webhook subscription, URL - event pair not found. ```json Status: 204 Body: none ``` An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "status": { "status": "ERROR", "code": "E_SESSION", "msg": "The API key you've entered is incorrect or no longer valid. Check if you pasted the key correctly. You can generate a new key in Woodpecker: Settings -> API Keys." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | Please review the [request URL](#endpoint) ```json { "status": { "status": "ERROR", "code": "E_URL_NOT_FOUND", "msg": "URL not found: /Woodpecker/rest/v1/webhooks/someMadeUpURL" } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | An unknown error. Please try again later. ```json { "status": { "status": "ERROR", "code": "E_UNNOWN", "msg": "Unknown error." } } ``` #### Body schema | Field | Data Type | Description | |-------------|-----------|--------------------------------------------------| | `status` | object | Contains error details | | └─`status` | string | Overall status, set to `ERROR` for non-2xx responses | | └─`code` | string | Code indicating the error category | | └─`msg` | string | Descriptive error message | --- ## Campaign completed #### `campaign_completed` This event is triggered whenever a campaign under your account has completed. This means it has contacted all available prospects, and there are no prospects left in the queue. A campaign is automatically marked as completed 24 hours after the last prospect is contacted. ### Payload ```json [ { "campaign": { "campaign_id": 123456, "campaign_name": "SaaS in America", "sent_from": "jared.dunn@piedpiper.com", "sent_from_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ] }, "method": "campaign_completed", "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].campaign` | object | Contains campaign data | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `sent_from` | string | One of the campaign sending email addresses. If multiple are used, refer to `sent_from_emails` instead | |   └─ `sent_from_emails` | array[string] | List of campaign sending email addresses | | `[].method` | object | Webhook event type | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Campaign paused by Bounce Shield #### `campaign_paused_by_bounce_shield` This event is triggered whenever [Bounce Shield](https://woodpecker.co/help-center/en/articles/15228700) automatically pauses a running campaign because the campaign's bounce rate exceeded the configured threshold. The payload contains campaign data and the Bounce Shield threshold that caused the campaign to be paused. ### Payload ```json [ { "campaign": { "campaign_id": 123456, "campaign_name": "SaaS in America", "sent_from": "jared.dunn@piedpiper.com", "sent_from_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "bounce_rate_threshold": 3 }, "method": "campaign_paused_by_bounce_shield", "timestamp": "2026-05-26T12:00:00+0200", "firm_id": 456789 } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].campaign` | object | Contains campaign data | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `sent_from` | string | One of the campaign sending email addresses. If multiple are used, refer to `sent_from_emails` instead | |   └─ `sent_from_emails` | array[string] | List of campaign sending email addresses | |   └─ `bounce_rate_threshold` | integer | Bounce Shield threshold, a percentage, configured in the campaign settings | | `[].method` | string | Webhook event type | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | integer | ID of your Woodpecker account | --- ## Campaign email sent #### `campaign_sent` This event is triggered whenever a campaign email is sent to a prospect. The payload contains prospect data as well as the content of the sent email. ### Payload ```json [ { "method": "campaign_sent", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-21T11:25:40+0100", "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "email": { "id": 1307909126, "campaign_id": 2246375, "message_id": "", "name_from": "Jared Dunn", "email_from": "jared.dunn@piedpiper.com", "name_to": "Erlich Bachman", "email_to": "erlich@bachmanity.com", "email_cc": "", "email_bcc": "", "subject": "Subject line", "sent": "2025-03-21T11:25:40+0100", "host": "host.woodpecker.co", "number": 4, "step": 2, "message": "
The HTML content of a sent message
" }, "timestamp": "2025-03-21T11:25:42+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE` | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].email` | object | Contains sent email data | |   └─ `id` | integer | Unique identifier of a response | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `message_id` | string | Message ID assigned by SMTP server | |   └─ `name_from` | string | Name of the person on whose behalf a specific message is sent | |   └─ `email_from ` | string | Email address that sent a given email | |   └─ `name_to ` | string | Prospect's full name | |   └─ `email_to ` | string | Prospect's email address | |   └─ `email_cc ` | string | Carbon copy email added to the campaign | |   └─ `email_bcc ` | string | Blind carbon copy email added to the campaign | |   └─ `subject` | string | Email's subject line | |   └─ `sent` | string | Timestamp of sending the message (ISO 8601 format) | |   └─ `host` | string | Sending host | |   └─ `number` | integer | Number of email sent | |   └─ `step` | integer | Step in campaign from which a sent email comes from | |   └─ `message` | string | Sent message in HTML format | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect opened an email #### `email_opened` This event is triggered whenever a prospect opens a tracked email within your campaign. ### Payload ```json [ { "method": "email_opened", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "step": 2, "email_no": "#2", "open_count": 1, "open_date_latest": "2025-02-21T00:00:00+0100", "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information. | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user. | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE` | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered. | | `[].step` | integer | Step in campaign from which an open comes from | | `[].email_no` | string | Number of email sent from which an open comes from | | `[].open_count` | integer | Number of opens of this specific email | | `[].open_date_latest` | string | Timestamp of the latest email open (ISO 8601 format) | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Follow-up after autoreply #### `followup_after_autoreply` This event is triggered whenever you schedule the earliest date for the follow-up to be sent after receiving an autoresponse. ### Payload ```json [ { "method": "followup_after_autoreply", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "ACTIVE", "in_campaign": 1, "emails_sent": 2, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": "123456", "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2022-12-24T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet | |   └─ `followup_after` | string | The earliest date after which a prospect can be contacted. | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Get webhook subscriptions Retrieve a list of all subscribed webhooks for the authenticated account. Each webhook consists of a `target_url` and `event` To manage your existing subscriptions or subscribe to new events, please refer to [this guide](/docs/webhooks/webhooks.md). ## Request ### Endpoint ``` GET https://api.woodpecker.co/rest/v2/webhooks ``` ### Headers ``` x-api-key: {YOUR_API_KEY} ``` For details on how to authenticate your requests, please see the [authentication guide](/docs/getting-started/authentication.mdx). ### Request samples #### Retrieve a list of webhook subscriptions ```bash curl --request GET \ --url "https://api.woodpecker.co/rest/v2/webhooks" \ --header "x-api-key: {YOUR_API_KEY}" ``` ```python import requests def get_webhooks(): url = "https://api.woodpecker.co/rest/v2/webhooks" headers = { "x-api-key": "{YOUR_API_KEY}" } response = requests.get(url, headers=headers) if response.status_code == 200: return response.json() else: raise Exception(f"GET request failed: {response.status_code}, {response.text}") if __name__ == "__main__": try: data = get_webhooks() print("GET response:", data) except Exception as e: print("Error:", e) ``` ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class WoodpeckerApiClient { private static final String API_KEY = "{YOUR_API_KEY}"; private static final String URL = "https://api.woodpecker.co/rest/v2/webhooks"; public static void main(String[] args) { try { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(new URI(URL)) .header("x-api-key", API_KEY) .GET() .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { System.out.println("GET response: " + response.body()); } else { System.err.println("GET request failed: " + response.statusCode()); } } catch (Exception e) { e.printStackTrace(); } } } ``` ```js const axios = require('axios'); async function getWebhooks() { const url = 'https://api.woodpecker.co/rest/v2/webhooks'; const headers = { 'x-api-key': '{YOUR_API_KEY}' }; try { const response = await axios.get(url, { headers }); console.log('GET response:', response.data); } catch (error) { console.error('GET request failed:', error.response ? error.response.status : error.message); } } getWebhooks(); ``` ```php 'https://api.woodpecker.co/rest/v2/', 'headers' => [ 'x-api-key' => getenv('WOODPECKER_API_KEY'), ], ]); try { $response = $client->get('webhooks'); 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 Returns a list of webhooks associated with the account, if any are subscribed. The results include all webhooks under the account, regardless of which user created the subscription. ```json { "webhooks": [ { "target_url": "https://myurl.com/woodpeckerwebhooks", "event": "PROSPECT_INTERESTED" }, { "target_url": "https://hooks.zapier.com/hooks/catch/123456/", "event": "PROSPECT_REPLIED" }, { "target_url": "https://hook.eu1.make.com/abcd123", "event": "PROSPECT_REPLIED" } ] } ``` #### Body schema | Field | Type | Description| |----------------|----------|--------------------- | `webhooks[]` | array | A list of webhook objects, each containing the target URL and the event type| | └─`target_url` | string | The URL to which the webhook is sent | | └─`event` | string | The type of event in Woodpecker that will trigger the webhook. [Available webhooks](/docs/webhooks/webhooks.md#available-events) | A response if your account is not subscribed to any webhooks. ```json { "message": "You haven't subscribed to any webhooks." } ``` #### Body schema | Field | Type | Description | |-------------|----------|---------------------------------| | `message` | string | A message describing the response| An issue with authorization. Please review the [authorization guide](/docs/getting-started/authentication.mdx) ```json { "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. ```json { "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 | Unexpected error, please try again later ```json { "title": "Internal server error", "status": 500, "detail": "An unexpected error has occurred. Please try again later.", "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 | --- ## Prospect clicked on a link #### `link_clicked` This event is triggered whenever a prospect clicks on a tracked link within your email. Note that link tracking must be enabled in the campaign step configuration beforehand. The payload contains prospect data as well as the link information. ### Payload ```json [ { "method": "link_clicked", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "click_url": "https://google.com", "step": 2, "email_no": "#2", "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE` | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].click_url` | string | URL of a link that was clicked by the prospect | | `[].step` | integer | Step in campaign from which a click comes from | | `[].email_no` | string | Number of email sent from which a click comes from | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## LinkedIn account connected #### `linkedin_automation_account_connected` This event is triggered when a LinkedIn account is successfully connected to Woodpecker. ### Payload ```json [ { "method": "linkedin_automation_account_connected", "linkedin_account": { "id": 156898745, "session_status": "CONNECTED", "full_name": "Erlich Bachman", "linkedin_url": "linkedin.com/in/erlich-bachman-404xyz", "level": "PREMIUM" }, "timestamp": "2026-06-01T10:00:00+0200", "firm_id": 456789 } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | string | Webhook event type | | `[].linkedin_account` | object | Contains data about the connected LinkedIn account | |   └─ `id` | integer | Unique identifier of the LinkedIn account in Woodpecker. You can retrieve LinkedIn account IDs with the [Get a list of LinkedIn accounts](/docs/linkedin/get-linkedin-accounts.mdx) endpoint | |   └─ `session_status` | string | Current connection status of the account. For this event, the value is `CONNECTED` | |   └─ `full_name` | string | Full name associated with the LinkedIn account | |   └─ `linkedin_url` | string | LinkedIn profile URL of the account | |   └─ `level` | string | Subscription level of the account. Available values: `CLASSIC`, `PREMIUM`, `RECRUITER_LITE`, `SALES_NAVIGATOR` or `UNKNOWN` | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | integer | ID of your Woodpecker account | --- ## LinkedIn account disconnected #### `linkedin_automation_account_disconnected` This event is triggered when a LinkedIn account is disconnected from Woodpecker. ### Payload ```json [ { "method": "linkedin_automation_account_disconnected", "linkedin_account": { "id": 156898745, "session_status": "DISCONNECTED", "full_name": "Erlich Bachman", "linkedin_url": "linkedin.com/in/erlich-bachman-404xyz", "level": null }, "timestamp": "2026-06-01T11:00:00+0200", "firm_id": 456789 } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | string | Webhook event type | | `[].linkedin_account` | object | Contains data about the disconnected LinkedIn account | |   └─ `id` | integer | Unique identifier of the LinkedIn account in Woodpecker. You can retrieve LinkedIn account IDs with the [Get a list of LinkedIn accounts](/docs/linkedin/get-linkedin-accounts.mdx) endpoint | |   └─ `session_status` | string | Current connection status of the account. For this event, the value is `DISCONNECTED` | |   └─ `full_name` | string | Full name associated with the LinkedIn account | |   └─ `linkedin_url` | string | LinkedIn profile URL of the account | |   └─ `level` | null | Subscription level of the account. When an account disconnects, the returned level will be `null` | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | integer | ID of your Woodpecker account | --- ## Prospect autoreplied #### `prospect_autoreplied` This event is triggered whenever a prospect's response is detected or when their status is manually updated to `RESPONDED.` If a prospect who is already marked as `RESPONDED` sends another response, Woodpecker will detect it in your mailbox and trigger the webhook again. The payload contains prospect data as well as the response content. ### Payload ```json [ { "method": "prospect_autoreplied", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "AUTOREPLIED", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "email": { "id": 191867492, "mail_id": 123321, "subject": "Reply message subject", "sender": "Erlich from Bachmanity", "email": "erlich@bachmanity.com", "date": "2025-03-21T20:47:40+0100", "message": "reply content" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Campaign-related parameters (`campaign_id`, `campaign_name`, `campaign_email`, `campaign_emails`, `campaign_email_sent`) have values only if webhook was generated in a specific campaign. Fields that do not have a value are returned as an empty string or null. ::: :::info `email` object contains the presented data only if an email response triggered the webhook. Otherwise it is an empty object. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].email` | object | Contains response data | |   └─ `id` | integer | Unique identifier of a response | |   └─ `mail_id` | string | ID of the IMAP mailbox that received the message. Use [v2/mailboxes](/docs/mailboxes/get-mailbox.mdx) for more information | |   └─ `subject` | string | Subject line of the response | |   └─ `sender` | string | Respondent's name | |   └─ `email` | string | Respondent's email address | |   └─ `date` | string | Response date (ISO 8601 format) | |   └─ `message` | string | Response message in HTML format | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect blacklisted #### `prospect_blacklisted` This event is triggered whenever a prospect's status is changed to `BLACKLIST`. This applies to all actions, for example manual blacklisting, secondary response mechanism, account-wide blacklist. ### Payload ```json [ { "method": "prospect_blacklisted", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "", "status": "BLACKLIST", "in_campaign": 1, "emails_sent": 0, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 0, "step": 1, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Campaign-related parameters (`campaign_id`, `campaign_name`, `campaign_email`, `campaign_emails`, `campaign_email_sent`) have values only if webhook was generated in a specific campaign. Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object/null | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet. Not available in the payload if the status change was done manually on global (not campaign) level. | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect bounced #### `prospect_bounced` This event is triggered whenever a prospect's status is changed to `BOUNCED`. ### Payload ```json [ { "method": "prospect_bounced", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "OPT_OUT", "in_campaign": 1, "emails_sent": 1, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 1, "step": 1, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Campaign-related parameters (`campaign_id`, `campaign_name`, `campaign_email`, `campaign_emails`, `campaign_email_sent`) have values only if webhook was generated in a specific campaign. Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object/null | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet. Not available in the payload if the status change was done manually on global (not campaign) level. | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered. | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect interested #### `prospect_interested` This event is triggered whenever a prospect's interest level is updated to `INTERESTED`. It can be initiated by either a manual change or an AI-classified response. ### Payload ```json [ { "method": "prospect_interested", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "REPLIED", "in_campaign": 1, "emails_sent": 2, "imported": "saasinamerica.csv", "interested": "INTERESTED", "interest_level": { "level": "INTERESTED", "ai_detected": true }, "campaign_id": "123456", "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect invalid #### `prospect_invalid` This event is triggered whenever a prospect's status is changed to `INVALID`. Both manually as well as during the built-in email validation. ### Payload ```json [ { "method": "prospect_invalid", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "OPT_OUT", "in_campaign": 1, "emails_sent": 1, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 1, "step": 1, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Campaign-related parameters (`campaign_id`, `campaign_name`, `campaign_email`, `campaign_emails`, `campaign_email_sent`) have values only if webhook was generated in a specific campaign. Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object/null | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet. Not available in the payload if the status change was done manually on global (not campaign) level. | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered. | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## LinkedIn connection request accepted #### `linkedin_automation_connection_request_accepted` This event is triggered when a prospect accepts a LinkedIn connection request sent through Woodpecker LinkedIn automation. ### Payload ```json [ { "method": "linkedin_automation_connection_request_accepted", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2026-03-20T14:32:34+0100", "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 1, "step": 2, "step_type": "LINKEDIN_CONNECTION_REQUEST", "followup_after": "2026-03-22T00:00:00+0100" }, "linkedin_action": { "id": 156898745, "campaign_id": 123456, "number": 4, "step": 2, "completed": "2026-03-20T14:32:34+0100", "type": "CONNECTION_REQUEST", "account_id": 111112 }, "timestamp": "2026-03-22T10:00:00+0100", "firm_id": 4567890 } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object/null | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE` | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].linkedin_action` | object | Contains data about the sent LinkedIn action | |   └─ `id` | integer | Unique identifier of a sent connection request | |   └─ `account_id` | integer | ID of the LinkedIn account | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `completed` | string | Timestamp of sending the connection request (ISO 8601 format) | |   └─ `type` | string | Type fo the sent Linkedin action: `CONNECTION_REQUEST` | |   └─ `step` | integer | Step in campaign from which a connection request comes from | |   └─ `number` | integer | Number of connection request step. Helps understand yes/not path of a campaign | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## LinkedIn direct message sent #### `linkedin_automation_direct_message_sent` This event is triggered when Woodpecker sends a LinkedIn direct message through LinkedIn automation. The payload contains prospect data as well as metadata about the sent direct message. To receive notification when a prospect replies, subscribe to [linkedin_automation_prospect_replied](prospect-li-replied.mdx). ### Payload ```json [ { "method": "linkedin_automation_direct_message_sent", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2026-03-20T14:32:34+0100", "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 1, "step": 2, "step_type": "LINKEDIN_DIRECT_MESSAGE", "followup_after": "2026-03-22T00:00:00+0100" }, "direct_message": { "id": 156898745, "campaign_id": 123456, "number": 4, "step": 2, "account_id": 111112, "linkedin_name_from": "Jared Dunn", "linkedin_url_from": "https://www.linkedin.com/in/jareds-account-xyz123", "sent": "2026-03-20T14:32:34+0100", "message": "Hello Erlich, thanks for connecting. I wanted to share a quick idea for Bachmanity.", "target_profile_url": "https://linkedin.com/erlich-bachman-404xyz" }, "timestamp": "2026-03-22T10:00:00+0100", "firm_id": 4567890 } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object/null | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE` | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].direct_message` | object | Contains data about the sent LinkedIn direct message | |   └─ `id` | integer | Unique identifier of a sent direct message | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `number` | integer | Number of direct message step. Helps understand yes/no path of a campaign | |   └─ `step` | integer | Step in campaign from which the direct message comes | |   └─ `account_id` | integer | ID of the LinkedIn account used to send the message | |   └─ `linkedin_name_from` | string/null | Full name of the LinkedIn sender account | |   └─ `linkedin_url_from` | string | LinkedIn profile URL of the sender account | |   └─ `sent` | string | Timestamp of sending the direct message (ISO 8601 format) | |   └─ `message` | string | Sent LinkedIn direct message content | |   └─ `target_profile_url` | string | LinkedIn profile URL of the message recipient | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## LinkedIn prospect replied #### `linkedin_automation_prospect_replied` Triggered when Woodpecker detects a prospect's response in a conversation tracked by LinkedIn automation, including replies to direct messages, InMail and message replies to a connection request with a note. Each newly detected Linkedin reply triggers this event. :::note Reply detection for InMail, and therefore InMail reply webhooks, is supported for LinkedIn Premium accounts but not for Sales Navigator accounts. ::: ### Payload ```json [ { "method": "linkedin_automation_prospect_replied", "replied": { "prospect_id": 1234567890, "prospect_linkedin_profile": "https://www.linkedin.com/in/erlich-bachman-404xyz", "campaign_id": 123456, "sent_id": 156898745, "orig_sent_action_type": "DIRECT_MESSAGE", "account_id": 111112, "from_name": "Erlich Bachman", "from_profile_id": "123456789", "linkedin_thread_url": "https://www.linkedin.com/messaging/thread/2-example-thread-id", "id": 987654321, "stamp": "2026-09-07T12:47:58+0200", "recipient": "https://www.linkedin.com/in/jareds-account-xyz123", "body": "Thanks Jared, I would be happy to hear more about your idea." }, "timestamp": "2026-09-07T13:31:03+0200", "firm_id": 4567890 } ] ``` ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | string | Webhook event type: `linkedin_automation_prospect_replied` | | `[].replied` | object | Contains the detected LinkedIn reply and its associated identifiers | |   └─ `prospect_id` | integer | Unique identifier of a prospect | |   └─ `prospect_linkedin_profile` | string | Prospect's LinkedIn profile URL | |   └─ `campaign_id` | integer | Unique identifier of the campaign associated with the response | |   └─ `sent_id` | integer | ID of the sent LinkedIn action associated with the reply | |   └─ `orig_sent_action_type` | string | Type of the associated sent action: `DIRECT_MESSAGE`, `INMAIL_MESSAGE`, `CONNECTION_REQUEST` (response to a CR that started the conversation, not acceptance of CR) | |   └─ `account_id` | integer | ID of the LinkedIn account in Woodpecker. See [LinkedIn accounts](/docs/linkedin/get-linkedin-accounts.mdx) for account details | |   └─ `from_name` | string | Respondents full name | |   └─ `from_profile_id` | string | Respondents LinkedIn profile ID | |   └─ `linkedin_thread_url` | string | URL of the conversation on LinkedIn | |   └─ `id` | integer | Unique ID of the LinkedIn reply in Woodpecker | |   └─ `stamp` | string | Reply timestamp in ISO 8601 format, including offset | |   └─ `recipient` | string | LinkedIn profile URL of the account that received the reply | |   └─ `body` | string | Reply message content | | `[].timestamp` | string | Timestamp of triggering the webhook in ISO 8601 format, including the UTC offset | | `[].firm_id` | integer | ID of your Woodpecker account | --- ## Prospect maybe later #### `prospect_maybe_later` This event is triggered whenever a prospect's interest level is updated to `MAYBE_LATER`. It can be initiated by either a manual change or an AI-classified response. ### Payload ```json [ { "method": "prospect_maybe_later", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "REPLIED", "in_campaign": 1, "emails_sent": 2, "imported": "saasinamerica.csv", "interested": "MAYBE_LATER", "interest_level": { "level": "MAYBE_LATER", "ai_detected": true }, "campaign_id": "123456", "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect nonresponsive #### `prospect_non_responsive` This event is triggered when a prospect's campaign status changes to `NON_RESPONSIVE`. This indicates that the prospect has gone through all campaign steps while remaining in the `ACTIVE` status and not responding. ### Payload ```json [ { "method": "prospect_non_responsive", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "NON_RESPONSIVE", "in_campaign": 1, "emails_sent": 2, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE` | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect not interested #### `prospect_not_interested` This event is triggered whenever a prospect's interest level is updated to `NOT_INTERESTED`. It can be initiated by either a manual change or an AI-classified response. ### Payload ```json [ { "method": "prospect_not_interested", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "REPLIED", "in_campaign": 1, "emails_sent": 2, "imported": "saasinamerica.csv", "interested": "NOT_INTERESTED", "interest_level": { "level": "NOT_INTERESTED", "ai_detected": true }, "campaign_id": "123456", "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect opt-out #### `prospect_opt_out` This event is triggered whenever a prospect's status is changed to `OPT_OUT`. This applies to the prospect using the unsubscribe link or manually changing the status. ### Payload ```json [ { "method": "prospect_opt_out", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "OPT_OUT", "in_campaign": 1, "emails_sent": 1, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 1, "step": 1, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Campaign-related parameters (`campaign_id`, `campaign_name`, `campaign_email`, `campaign_emails`, `campaign_email_sent`) have values only if webhook was generated in a specific campaign. Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object/null | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet. Not available in the payload if the status change was done manually on global (not campaign) level | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect replied #### `prospect_replied` This event is triggered whenever a prospect's response is detected or when their status is manually updated to `RESPONDED`. If a prospect who is already marked as `RESPONDED` sends another response, Woodpecker will detect it in your mailbox and trigger the webhook again. The payload contains prospect data as well as the response content. ### Payload ```json [ { "method": "prospect_replied", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "REPLIED", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "INTERESTED", "interest_level": { "level": "INTERESTED", "ai_detected": true }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "email": { "id": 191867492, "mail_id": 123321, "subject": "Reply message subject", "sender": "Erlich from Bachmanity", "email": "erlich@bachmanity.com", "date": "2025-03-21T20:47:40+0100", "message": "reply content" }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Campaign-related parameters (`campaign_id`, `campaign_name`, `campaign_email`, `campaign_emails`, `campaign_email_sent`) have values only if webhook was generated in a specific campaign. Fields that do not have a value are returned as an empty string or null. ::: :::info `step_type` field is not available in the payload if the status change was done manually on global (not campaign) level. Similarly, `email` object contains the presented data only if an email response triggered the webhook. Otherwise it is an empty object. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object/null | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet. Not available in the payload if the status change was done manually on global (not campaign) level | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].email` | object | Contains response data. If the status change was made manually rather than triggered by Woodpecker detecting a prospect's response, this object will be empty | |   └─ `id` | integer | Unique identifier of a response | |   └─ `mail_id` | string | ID of the IMAP mailbox that received the message. Use [v2/mailboxes](/docs/mailboxes/get-mailbox.mdx) for more information | |   └─ `subject` | string | Subject line of the response | |   └─ `sender` | string | Respondent's name | |   └─ `email` | string | Respondent's email address | |   └─ `date` | string | Response date (ISO 8601 format) | |   └─ `message` | string | Response message in HTML format | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Prospect saved #### `prospect_saved` This event is triggered whenever a new prospect is added to your database or there's an update of an existing one. The triggers for this event include updating snippet data or manually changing the global status. Note that changes to local (campaign) status or automatic status updates after a response do not trigger this event. ### Payload ```json [ { "method": "prospect_saved", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "ACTIVE", "in_campaign": 1, "emails_sent": 1, "imported": "saasinamerica.csv", "interested": "", "interest_level": null, "campaign_id": "", "campaign_name": "", "campaign_email": "", "campaign_emails": [], "campaign_email_sent": null, "step": null }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Campaign-related parameters (`campaign_id`, `campaign_name`, `campaign_email`, `campaign_emails`, `campaign_email_sent`) have values only if webhook was generated in a specific campaign. Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Empty string | |   └─ `interest_level` | null | Prospect's Interest Level information. For this webhook it will always be `null` | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Secondary response #### `secondary_replied` This event is triggered whenever a [secondary response](https://woodpecker.co/help-center/en/articles/5241870) is detected. `prospect` object contains the data of a prospect you contacted in a campaign; `secondary_prospect` contains the data of a person who responded from a different email. The payload contains prospect data and the secondary prospect's data. If you'd like to fetch the response content, use the [responses endpoint](/docs/prospects/GET-prospect-responses.mdx). ### Payload ```json [ { "method": "secondary_replied", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-03-20T14:32:34+0100", "status": "REPLIED", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": 123456, "campaign_name": "SaaS in America", "campaign_email": "jared.dunn@piedpiper.com", "campaign_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ], "campaign_email_sent": 2, "step": 2, "step_type": "EMAIL", "followup_after": "2025-03-22T00:00:00+0100" }, "secondary_prospect": { "id": 1234567896, "email": "jian@bachmanity.com", "first_name": "", "last_name": "", "company": "", "website": "", "linkedin_url": "", "tags": "#SECONDARYEMAIL", "title": "", "phone": "", "address": "", "city": "", "country": "", "snippet1": "", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "" }, "industry": "", "state": "", "last_contacted": "", "status": "BLACKLIST", "in_campaign": null, "emails_sent": null, "imported": "", "interested": "", "interest_level": null, "campaign_id": "", "campaign_name": "", "campaign_email": "", "campaign_emails": "", "campaign_email_sent": null, "step": null, "step_type": null }, "timestamp": "2025-03-21T20:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object/null | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | integer | Unique identifier of the campaign | |   └─ `campaign_name` | string | Name of the campaign | |   └─ `campaign_email` | string | One of the campaign sending email addresses. If multiple are used, refer to `campaign_emails` instead | |   └─ `campaign_emails` | array[string] | List of campaign sending email addresses | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer/null | Prospect's current step in campaign | |   └─ `step_type` | string/null | Step type the prospect is currently on: `EMAIL`, `MANUAL_TASK`, `LINKEDIN_VISIT_PROFILE`, `LINKEDIN_CONNECTION_REQUEST`, `LINKEDIN_DIRECT_MESSAGE`. Null if a prospect hasn't been contacted yet | |   └─ `followup_after` | string/null | The earliest date after which a prospect can be contacted. Primarily used for follow-ups after an autoresponse. Field is available only if its value was not null when the webhook was triggered | | `[].secondary_prospect` | object | Contains secondary prospect's data. Contains the same fields as the `prospect` object but most of them are empty strings or nulls (as shown in the example body). Only fields that will always contain a value are described below | |   └─ `id` | integer | Unique identifier of a secondary prospect | |   └─ `email` | string | Secondary prospect's email address | |   └─ `tags` | string | By default, secondary prospects will have a tag `#SECONDARYEMAIL` | |   └─ `status` | string | By default, secondary prospects have the status `BLACKLIST` assigned | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Task created #### `task_created` This event is triggered whenever a manual task for a prospect is created. ### Payload ```json [ { "method": "task_created", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-01-11T12:18:54+0100", "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": "", "campaign_name": "", "campaign_email": "", "campaign_emails": "", "campaign_email_sent": 0, "step": 1 }, "campaign": { "campaign_id": 11223344, "campaign_name": "SaaS in America", "sent_from": "jared.dunn@piedpiper.com", "sent_from_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ] }, "task": { "type": "LINKEDIN", "name": "Send connection request", "message": "Hi Erlich, I'd like to join your connection network.", "due_date": "2025-03-24T15:47:47+0100" }, "timestamp": "2025-03-21T15:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. `campaign.sent_from` field is available only if the campaign has at least one email step. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_name` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_email` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_emails` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer | Prospect's current step in campaign | | `[].campaign` | object | Contains campaign data | |   └─ `campaign_id`| integer | Unique identifier of the campaign | |   └─ `campaign_name`| string | Name of the campaign | |   └─ `sent_from`| string | One of the campaign sending email addresses. If multiple are used, refer to `sent_from_emails` instead. The field is available only if the campaign has at least one email step | |   └─ `sent_from_emails`| array[string] | List of campaign sending email addresses | | `[].task` | object | Contains task data | |   └─ `type` | string | Task type. Available types: `GENERIC`, `CALL`, `SMS`, `LINKEDIN` | |   └─ `name` | string | Task name | |   └─ `message` | string | Task message or description | |   └─ `due_date` | string | Task due date in (ISO 8601 format) | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Task done #### `task_done` This event is triggered whenever a manual task for a prospect is marked as done. ### Payload ```json [ { "method": "task_done", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-01-11T12:18:54+0100", "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": "", "campaign_name": "", "campaign_email": "", "campaign_emails": "", "campaign_email_sent": 0, "step": 1 }, "campaign": { "campaign_id": 11223344, "campaign_name": "SaaS in America", "sent_from": "jared.dunn@piedpiper.com", "sent_from_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ] }, "task": { "type": "LINKEDIN", "name": "Send connection request", "message": "Hi Erlich, I'd like to join your connection network.", "due_date": "2025-03-24T15:47:47+0100" }, "timestamp": "2025-03-21T15:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. `campaign.sent_from` field is available only if the campaign has at least one email step. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_name` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_email` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_emails` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer | Prospect's current step in campaign | | `[].campaign` | object | Contains campaign data | |   └─ `campaign_id`| integer | Unique identifier of the campaign | |   └─ `campaign_name`| string | Name of the campaign | |   └─ `sent_from`| string | One of the campaign sending email addresses. If multiple are used, refer to `sent_from_emails` instead. The field is available only if the campaign has at least one email step | |   └─ `sent_from_emails`| array[string] | List of campaign sending email addresses | | `[].task` | object | Contains task data | |   └─ `type` | string | Task type. Available types: `GENERIC`, `CALL`, `SMS`, `LINKEDIN` | |   └─ `name` | string | Task name | |   └─ `message` | string | Task message or description | |   └─ `due_date` | string | Task due date in (ISO 8601 format) | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Task ignored #### `task_ignored` This event is triggered whenever a manual task for a prospect is marked as ignored. ### Payload ```json [ { "method": "task_ignored", "prospect": { "id": 1234567890, "email": "erlich@bachmanity.com", "first_name": "Erlich", "last_name": "Bachman", "company": "Bachmanity", "website": "https://bachmanity.com", "linkedin_url": "https://linkedin.com/erlich-bachman-404xyz", "tags": "#VISIONARY", "title": "CEO", "phone": "+1 987-654-321", "address": "700 Welch Road", "city": "Palo Alto", "country": "United States", "snippet1": "You are running a successful startup incubator Bachmanity", "snippet2": "", "snippet3": "", "snippet4": "", "snippet5": "", "snippet6": "", "snippet7": "", "snippet8": "", "snippet9": "", "snippet10": "", "snippet11": "", "snippet12": "", "snippet13": "", "snippet14": "", "snippet15": "", "snippet_labels": { "my snippet label": "You are running a successful startup incubator Bachmanity" }, "industry": "IT", "state": "California", "last_contacted": "2025-01-11T12:18:54+0100", "status": "ACTIVE", "in_campaign": 2, "emails_sent": 3, "imported": "saasinamerica.csv", "interested": "", "interest_level": { "level": "NOT_MARKED", "ai_detected": false }, "campaign_id": "", "campaign_name": "", "campaign_email": "", "campaign_emails": "", "campaign_email_sent": 0, "step": 1 }, "campaign": { "campaign_id": 11223344, "campaign_name": "SaaS in America", "sent_from": "jared.dunn@piedpiper.com", "sent_from_emails": [ "jared.dunn@piedpiper.com", "richard.hendricks@piedpiper.com", "jian@bachmanity.com" ] }, "task": { "type": "LINKEDIN", "name": "Send connection request", "message": "Hi Erlich, I'd like to join your connection network.", "due_date": "2025-03-24T15:47:47+0100" }, "timestamp": "2025-03-21T15:47:47+0100", "firm_id": 456789 } ] ``` :::note Fields that do not have a value are returned as an empty string or null. `campaign.sent_from` field is available only if the campaign has at least one email step. ::: ### Body schema | Field | Type | Description | |-------|------|-------------| | `[].method` | object | Webhook event type | | `[].prospect` | object | Contains prospect data | |   └─ `id` | integer | Unique identifier of a prospect | |   └─ `email` | string | Prospect's email address | |   └─ `first_name` | string | Prospect's first name | |   └─ `last_name` | string | Prospect's last name | |   └─ `company` | string | Prospect's company name | |   └─ `website` | string | Prospect's website URL | |   └─ `linkedin_url` | string | Prospect's LinkedIn profile URL | |   └─ `tags` | string | Tags associated with the prospect. Tags start with a `#` and are separated with a space | |   └─ `title` | string | Prospect's job title | |   └─ `phone` | string | Prospect's phone number | |   └─ `address` | string | Prospect's address | |   └─ `city` | string | Prospect's city | |   └─ `country` | string | Prospect's country | |   └─ `snippet` | string | Prospect custom snippets. There are 15 snippet fields (`snippet1` to `snippet15`) | |   └─ `snippet_labels` | object | Custom snippet labels | |     └─└─ `label_name` | string | Key - value pairs representing a snippet label and its value | |   └─ `industry` | string | Prospect's industry | |   └─ `state` | string | Prospect's state or region | |   └─ `last_contacted` | string | Date when the prospect was last contacted (ISO 8601 format) | |   └─ `status` | string | Prospect's status | |   └─ `in_campaign` | integer | Total number of campaigns the prospect is enrolled in | |   └─ `emails_sent` | integer | Total number of emails sent to the prospect from all campaigns | |   └─ `imported` | string | Name of a file prospect was imported from | |   └─ `interested` | string | Deprecated. Prospect's Interest Level status. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `""` | |   └─ `interest_level` | object | Prospect's Interest Level information | |     └─└─ `level` | string | Prospect's Interest Level. Available levels: `INTERESTED`, `MAYBE_LATER`, `NOT_INTERESTED` or `NOT_MARKED` | |     └─└─ `ai_detected` | boolean | Indicates whether the `level` was set by AI or by the user | |   └─ `campaign_id` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_name` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_email` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_emails` | string | Empty string. Please refer to the `campaign` object instead | |   └─ `campaign_email_sent` | integer/null | Number of emails sent from the specific campaign the webhook comes from | |   └─ `step` | integer | Prospect's current step in campaign | | `[].campaign` | object | Contains campaign data | |   └─ `campaign_id`| integer | Unique identifier of the campaign | |   └─ `campaign_name`| string | Name of the campaign | |   └─ `sent_from`| string | One of the campaign sending email addresses. If multiple are used, refer to `sent_from_emails` instead. The field is available only if the campaign has at least one email step | |   └─ `sent_from_emails`| array[string] | List of campaign sending email addresses | | `[].task` | object | Contains task data | |   └─ `type` | string | Task type. Available types: `GENERIC`, `CALL`, `SMS`, `LINKEDIN` | |   └─ `name` | string | Task name | |   └─ `message` | string | Task message or description | |   └─ `due_date` | string | Task due date in (ISO 8601 format) | | `[].timestamp` | string | Timestamp of triggering the webhook (ISO 8601 format) | | `[].firm_id` | string | ID of your Woodpecker account | --- ## Webhooks ## Overview You can utilize webhooks to get notified about events related to your prospects and campaigns. You will receive a notification when a prospect replies, is marked as interested, your campaign has finishes, etc. All notifications are triggered at the account level. For instance, when you subscribe to the `prospect_replied` event, you will receive data about replies from prospects across all campaigns associated with your account. ### Managing webhook subscription To subscribe or unsubscribe from webhooks, send a POST request with the `target_url` and `event` specified in the body. You can find detailed instructions here: * [Subscribe to webhook](POST-subscribe-webhook.mdx) * [Unsubscribe from a webhook](POST-unsubscribe-webhook.mdx) ### Subscription limits You can subscribe to the same event up to five times per account, provided that each subscription uses a unique `target_url`. Attempting to subscribe to the same event more times will result in a `409` response. You can subscribe multiple different events to one target URL. To review your current subscriptions, use the [GET v2/webhooks endpoint](get-webhooks.mdx). ### Error handling All webhooks are sent using HTTP POST method. Should the receiving server return an error, for example `500` code response, Woodpecker uses an exponential backoff retry mechanism to attempt delivery of the data. Multiple subsequent failed delivery attempts may result in unsubscribing a given `target_url` and `event` pair. A webhook event will also be automatically unsubscribed if the receiving servers responds with HTTP codes `310` or `410`. ### Batch delivery We use a batching mechanism that groups multiple prospect events into a single webhook payload when they occur within a short time window. Events are grouped only if they share the same `event` type and `target_url`. The payload is sent as an array of objects, with each object representing one event or prospect's data. The maximum array size is 100 objects. ## Available events * [campaign_completed](campaign-completed.mdx) - when a campaign status is changed to `COMPLETED` * [campaign_paused_by_bounce_shield](campaign-paused-by-bounce-shield.mdx) - when [Bounce Shield](https://woodpecker.co/help-center/en/articles/15228700) automatically pauses a campaign because its bounce rate exceeded the configured threshold * [campaign_sent](campaign-sent.mdx) - when a campaign email is sent to a prospect * [email_opened](email-opened.mdx) - when a prospect opens your email * [followup_after_autoreply](followup-after-autoreply.mdx) - when you schedule a follow-up after receiving an autoresponse * [link_clicked](link-clicked.mdx) - when a prospect clicks on a tracked link within your email * [linkedin_automation_account_connected](linkedin-account-connected.mdx) - when a LinkedIn account is successfully connected to Woodpecker * [linkedin_automation_account_disconnected](linkedin-account-disconnected.mdx) - when a LinkedIn account is disconnected from Woodpecker * [linkedin_automation_connection_request_accepted](prospect-li-cr-accepted.mdx) - when a prospect accepts a LinkedIn connection request sent through Woodpecker LinkedIn automation * [linkedin_automation_direct_message_sent](prospect-li-dm-sent.mdx) - when a LinkedIn direct message is sent through Woodpecker LinkedIn automation * [linkedin_automation_prospect_replied](prospect-li-replied.mdx) - when a prospect replies in a conversation tracked by LinkedIn automation, including direct message, InMail and message responses to connection requests * [prospect_autoreplied](prospect-autoreplied.mdx) - when an autoreply is detected or prospect's status is manually changed to `AUTOREPLIED` * [prospect_blacklisted](prospect-blacklisted.mdx) - when a prospect's status is updated to `BLACKLISTED` * [prospect_bounced](prospect-bounced.mdx) - when a bounce is detected or prospect's status is manually changed to `BOUNCED` * [prospect_interested](prospect-interested.mdx) - when prospect's interest level is updated to `INTERESTED` * [prospect_invalid](prospect-invalid.mdx) - when prospect's email address is marked as `INVALID` during the built-in email validation, or manually * [prospect_maybe_later](prospect-maybe-later.mdx) - when prospect's interest level is updated to `MAYBE_LATER` * [prospect_non_responsive](prospect-non-responsive.mdx) - when a prospect's campaign status is changed to `NON_RESPONSIVE` * [prospect_not_interested](prospect-not-interested.mdx) - when prospect's interest level is updated to `NOT_INTERESTED` * [prospect_opt_out](prospect-opt-out.mdx) - when a prospect unsubscribes or their status is manually changed to `OPT_OUT` * [prospect_replied](prospect-replied.mdx) - when a prospect's reply is detected or their status is manually updated to `RESPONDED` * [prospect_saved](prospect-saved.mdx) - when a new prospect is added to your database or there's an update of an existing one * [secondary_replied](secondary-replied.mdx) - when a [secondary response](https://woodpecker.co/help-center/en/articles/5241870) is detected * [task_created](task-created.mdx) - when a manual task is created for a prospect * [task_done](task-done.mdx) - when a manual task is marked as done * [task_ignored](task_ignored.mdx) - when a manual task is marked as ignored --- ## Available tools ## Campaign management #### `createEmailCampaign` Create a Woodpecker email campaign with one email step and minimal configuration. | Parameter | Type | Description | |-------------------|------------------|---------------------------------------| | `name` | string, optional | Campaign name | | `timezone` | string | Campaign timezone | | `dailyEnroll` | integer | Daily prospect enrollment limit | | `emailAccountIds` | array | SMTP account IDs | | `versions` | array | Email versions with `subject` and `body` (max 5) | | `deliveryTimes` | object | Required delivery schedule with `deliveryDays`, `deliveryTimeStart`, and `deliveryTimeStop` | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | #### `createLinkedinProfileVisitCampaign` Create a Woodpecker LinkedIn campaign with one profile visit step. | Parameter | Type | Description | |---------------------|------------------|---------------------------------------| | `name` | string, optional | Campaign name | | `timezone` | string | Campaign timezone | | `dailyEnroll` | integer | Daily prospect enrollment limit | | `linkedinAccountId` | integer | LinkedIn account ID | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | #### `createLinkedinConnectionRequestCampaign` Create a Woodpecker LinkedIn campaign with one connection request step. | Parameter | Type | Description | |---------------------|------------------|--------------------------------------------------------------------------------------------------------| | `name` | string, optional | Campaign name | | `timezone` | string | Campaign timezone | | `dailyEnroll` | integer | Daily prospect enrollment limit | | `linkedinAccountId` | integer | LinkedIn account ID | | `bodyVersions` | array, optional | Connection request message strings (max 5; empty/null sends the connection request without a message) | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | #### `createLinkedinDirectMessageCampaign` Create a Woodpecker LinkedIn campaign with one direct message step. | Parameter | Type | Description | |---------------------|------------------|-----------------------------------------| | `name` | string, optional | Campaign name | | `timezone` | string | Campaign timezone | | `dailyEnroll` | integer | Daily prospect enrollment limit | | `linkedinAccountId` | integer | LinkedIn account ID | | `bodyVersions` | array | Direct message strings (max 5) | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | #### `createLinkedinInMailMessageCampaign` Create a Woodpecker LinkedIn campaign with one InMail message step. | Parameter | Type | Description | |---------------------|------------------|---------------------------------------------------------------| | `name` | string, optional | Campaign name | | `timezone` | string | Campaign timezone | | `dailyEnroll` | integer | Daily prospect enrollment limit | | `linkedinAccountId` | integer | LinkedIn account ID | | `bodyVersions` | array | InMail versions with `subject` and `body` | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | #### `listCampaigns` Retrieve campaigns with optional status filtering. | Parameter | Type | Description | |--------------|---------|-----------------------------------------------------------------------------------| | `pageNumber` | integer | Page number (1-based) | | `statuses` | array | Filter by status (`RUNNING`, `DRAFT`, `EDITED`, `PAUSED`, `STOPPED`, `COMPLETED`) | #### `retrieveCampaignDetails` Get detailed campaign structure including all steps and configurations. | Parameter | Type | Description | |--------------|---------|---------------| | `campaignId` | integer | Campaign ID | #### `retrieveCampaignStatistics` Fetch campaign performance metrics and analytics. | Parameter | Type | Description | |--------------|---------|---------------| | `campaignId` | integer | Campaign ID | #### `updateCampaignSettings` Modify campaign-wide settings including name, email accounts, daily limits and timezone. | Parameter | Type | Description | |-------------------|------------------|---------------------------| | `campaignId` | integer | Campaign ID | | `name` | string | Campaign name | | `emailAccountIds` | array | List of email account IDs | | `timezone` | string | Campaign timezone | | `dailyEnroll` | integer | Daily enrollment limit | #### `buildCampaignUrl` Generate Woodpecker app URL for campaign access. | Parameter | Type | Description | |--------------|---------|---------------| | `campaignId` | integer | Campaign ID | Campaign control `runCampaign(campaignId)` - Start campaign execution `pauseCampaign(campaignId)` - Pause campaign `stopCampaign(campaignId)` - Stop campaign `deleteCampaign(campaignId)` - Remove campaign entirely `makeCampaignEditable(campaignId)` - Enable campaign modifications ## Step management #### `addEmailStep` Add an email follow-up step to an existing campaign. | Parameter | Type | Description | |-----------------|------------------|---------------------------------------| | `campaignId` | integer | Campaign ID | | `parentId` | string | Parent step ID | | `versions` | array | Email versions with `subject` and `body` (max 5) | | `deliveryTimes` | object | Required delivery schedule with `deliveryDays`, `deliveryTimeStart`, and `deliveryTimeStop` | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | #### `addLinkedinProfileVisitStep` Add a LinkedIn profile visit follow-up step to an existing campaign. | Parameter | Type | Description | |----------------------|------------------|---------------------------------------| | `campaignId` | integer | Campaign ID | | `parentId` | string | Parent step ID | | `linkedinAccountId` | integer | LinkedIn account ID | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | #### `addLinkedinConnectionRequestStep` Add a LinkedIn connection request follow-up step to an existing campaign. | Parameter | Type | Description | |----------------------|------------------|--------------------------------------------------------------------------------------------------------| | `campaignId` | integer | Campaign ID | | `parentId` | string | Parent step ID | | `linkedinAccountId` | integer | LinkedIn account ID | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | | `bodyVersions` | array, optional | Connection request message strings (max 5; empty/null sends the connection request without a message) | #### `addLinkedinDirectMessageStep` Add a LinkedIn direct message follow-up step to an existing campaign. | Parameter | Type | Description | |----------------------|------------------|-----------------------------------------| | `campaignId` | integer | Campaign ID | | `parentId` | string | Parent step ID | | `linkedinAccountId` | integer | LinkedIn account ID | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | | `bodyVersions` | array | Direct message strings (max 5) | #### `addLinkedinInMailMessageStep` Add a LinkedIn InMail message follow-up step to an existing campaign. | Parameter | Type | Description | |---------------------|------------------|---------------------------------------------------------------| | `campaignId` | integer | Campaign ID | | `parentId` | string | Parent step ID | | `linkedinAccountId` | integer | LinkedIn account ID | | `bodyVersions` | array | InMail versions with `subject` and `body` | | `followupAfter` | object, optional | Delay with `range` (`DAY`, `HOUR`, or `MINUTE`) and `value` | #### `updateCampaignStep` Modify step delivery times and scheduling. | Parameter | Type | Description | |--------------|---------|--------------------------------| | `campaignId` | integer | Campaign ID | | `stepId` | string | Step ID | | `payload` | string | JSON string with `delivery_time` for each day | #### `updateEmailStepVersion` Update email content, subject lines, signatures and tracking settings. | Parameter | Type | Description | |--------------|---------|-----------------------------| | `campaignId` | integer | Campaign ID | | `stepId` | string | Step ID | | `versionId` | string | Version ID | | `subject` | string | Email subject | | `message` | string | Email body (HTML supported) | | `signature` | string | `SENDER` or `NO_SIGNATURE` | | `trackOpens` | boolean | Enable open tracking | #### `updateLinkedinStepVersion` Update LinkedIn connection request, direct message, or InMail content. | Parameter | Type | Description | |--------------|------------------|------------------------------------------| | `campaignId` | integer | Campaign ID | | `stepId` | string | Step ID | | `versionId` | string | Version ID | | `message` | string | LinkedIn message content | | `subject` | string, optional | InMail subject; used only for InMail steps | #### `deleteCampaignStep` Remove steps from campaigns. | Parameter | Type | Description | |--------------|---------|-------------| | `campaignId` | integer | Campaign ID | | `stepId` | string | Step ID | ## Prospect management The `prospectsPayload` parameter is a JSON string containing an array of prospect objects. Each object supports `email`, `status`, `first_name`, `last_name`, `company`, `website`, `linkedin_url`, `tags`, `title`, `phone`, `address`, `city`, `state`, `country`, `industry`, and `snippet1` through `snippet15`. #### `addProspectsToDatabase` Adds new prospects to your global prospect list without enrolling them in any campaign. | Parameter | Type | Description | |-------------------|----------|--------------------------------| | `prospectsPayload`| string | JSON string containing an array of prospect objects | Notes: - Prospects are added to your account but not to any campaign - Available for future campaign enrollment - Useful for building a prospect database before campaign creation #### `addProspectsToCampaign` Bulk add prospects with full contact information and custom snippets. | Parameter | Type | Description | |--------------------|---------|---------------------------| | `campaignId` | integer | Campaign ID | | `prospectsPayload` | string | JSON string containing an array of prospect objects | Note: Always check for DUPLICATE prospects in response. Use `updateProspectsInCampaign` for duplicates if data updates are needed. #### `updateProspectsInDatabase` Updates existing prospects in your global database or adds new ones if they don't exist. | Parameter | Type | Description | |-------------------|----------|--------------------------------| | `prospectsPayload`| string | JSON string containing an array of prospect objects | Notes: - Existing prospects are updated based on email address - New prospects are added if email doesn't exist - Only include fields you want to update - Updates apply globally (affects all campaigns using these prospects) #### `updateProspectsInCampaign` Update existing prospect data (requires explicit user request). | Parameter | Type | Description | |--------------------|---------|---------------------------| | `campaignId` | integer | Campaign ID | | `prospectsPayload` | string | JSON string containing an array of prospect objects | #### `listProspectsInDatabase` Lists prospects from your global prospect database (not tied to any specific campaign). | Parameter | Type | Description | |---------------|----------|-----------------------| | `pageNumber` | integer | Page number (1-based) | Notes: - Returns paginated results of all prospects in your account - These prospects can be added to any campaign - Useful for managing your overall prospect database #### `listProspectsInCampaign` Paginated retrieval of campaign prospects. | Parameter | Type | Description | |---------------|----------|-----------------------| | `campaignId` | integer | Campaign ID | | `pageNumber` | integer | Page number (1-based) | #### `searchProspects` Searches for prospects that match specific criteria across your entire database. | Parameter | Type | Description | |------------------|-------------------|-------------------------------------| | `pageNumber` | integer | Page number (1-based) | | `searchCriteria` | object (optional) | JSON object with search parameters | | `filterCriteria` | object (optional) | JSON object with additional filters | **Available search fields:** - `email` - Email address - `first_name` - First name - `last_name` - Last name - `company` - Company name - `organization_id` - Organization ID - `industry` - Industry - `website` - Website URL - `tags` - Tags (case-sensitive, without leading #) - `title` - Job title - `phone` - Phone number - `address` - Street address - `city` - City - `state` - State/Province - `country` - Country - `snippet1` through `snippet15` - Custom fields **Available filter fields:** - `id` - Comma-separated list of prospect IDs - `status` - Prospect's global status: ACTIVE, BOUNCED, REPLIED, BLACKLIST, INVALID - `campaigns_id` - Comma-separated list of campaign IDs that prospects are enrolled in - `contacted` - Whether a prospect has ever been contacted - `interested` - Interest level: INTERESTED, MAYBE-LATER, NOT-INTERESTED, NOT-MARKED **Notes:** - Tag searching is case-sensitive, don't use leading # when searching by tags - Search criteria uses OR for same field, AND for different fields - To filter OPT-OUT prospects, use "BLACKLIST" status - Multiple filter values are comma-separated #### `deleteProspects` Permanently deletes prospects from your database and/or specific campaigns. | Parameter | Type | Description | |---------------|-------------------|---------------------------------------------------------------------| | `prospectIds` | string | Comma-separated list of prospect IDs to delete | | `campaignIds` | string (optional) | Comma-separated list of campaign IDs from which to remove prospects | Notes: - **Without campaignIds**: Deletes prospects globally from your entire database - **With campaignIds**: Removes prospects only from specified campaigns - This action is permanent and cannot be undone - Requires explicit user confirmation before execution - Use prospect IDs (not email addresses) obtained from list/search operations **Warning:** Global deletion removes prospects from all campaigns and your database. Local deletion (with campaignIds) only removes them from specified campaigns while keeping them in your global database. ## Account management #### `listMailboxes` Retrieve available email accounts for campaign assignment. | Parameter | Type | Description | |----------------|------------------|--------------------------------------------------------------| | `filter` | string | Filter to use when listing mailboxes (`SMTP`, `IMAP`, `ALL`) | #### `listLinkedinAccounts` Retrieve available LinkedIn accounts for campaign assignment. | Parameter | Type | Description | |----------------|------------------|------------------------------------------------------------------------| | `filter` | string | Filter to use when listing accounts (`AVAILABLE_FOR_CAMPAIGN`, `ALL`) | --- ## Connect Claude to the Woodpecker MCP server Connect Woodpecker to Claude in just a few steps - no local server or API key required. Once connected, you can chat with Claude to manage your campaigns wherever you work: on the web, in the desktop or mobile app, or in Claude Code. ## Connecting Claude Choose whether you want to connect through Claude or Claude Code. Setup steps vary by Claude plan. On Team and Enterprise plans, an Owner must first add the connector for the organization. See Claude's [custom connector documentation](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp#h_3d1a65aded) for the current plan-specific instructions. 1. In Claude, open **Settings** and select **Connectors**. 2. Select **Add**, then **Add custom connector**. 3. Enter a name for the connector, for example `Woodpecker`. 4. Enter the following remote MCP server URL: ```text https://api.woodpecker.co/mcp/v1 ``` Leave the OAuth Client ID and OAuth Client Secret fields empty. 5. Add the connector, then select **Connect**. 6. Sign in to your Woodpecker account when prompted. 7. Review the requested access and select **Allow**. After authorization, the connector settings show the available Woodpecker tools and let you control whether Claude can use each [tool](/docs/mcp/available-tools.mdx) automatically, with approval or not at all. To verify the connection, ask Claude: ```text "What Woodpecker tools do you have access to?" "List my Woodpecker campaigns" ``` Add the Woodpecker remote MCP server using the Claude Code CLI: ```bash claude mcp add --transport http woodpecker https://api.woodpecker.co/mcp/v1 ``` Then authenticate the connection: ```bash claude mcp login woodpecker ``` To verify that the server is connected, run: ```bash claude mcp list ``` You can also check the connection from within a Claude Code session by running: ```bash /mcp ``` See the official [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp#installing-mcp-servers) for additional configuration options. ## Troubleshoot the Claude connection The connector is not available **Symptoms**: You cannot find or add the Woodpecker connector * On a Team or Enterprise plan, ask an Owner to add the custom connector to the organization * After the Owner adds it, open **Settings** → **Connectors**, find Woodpecker and select **Connect** Authorization was not completed **Symptoms**: Woodpecker remains disconnected in Claude * Open **Settings** → **Connectors** and select the Woodpecker connector * Select **Connect**, sign in to the intended Woodpecker account and complete the authorization by selecting **Allow** The connector was disconnected after a period of inactivity **Symptoms**: A previously working Woodpecker connector is disconnected or Claude asks you to authorize it again * The authorization may expire if the connector has not been used for an extended period. When prompted by Claude, sign in to Woodpecker and authorize the connection again. * You can also reconnect it manually from Settings → Connectors. * In Claude Code, run `/mcp` and select **Woodpecker** Tools are not available in a conversation **Symptoms**: Claude is connected to Woodpecker but cannot use its tools * Enable the Woodpecker connector for the current conversation from the **+** menu * Open the connector settings and make sure the required tools are not disabled * If a tool is set to **Needs approval**, approve its use when Claude requests it An Agency client account cannot be selected **Symptoms**: You connected an Agency account but cannot access or switch to one of its client accounts. * The hosted Woodpecker MCP server does not currently support selecting client accounts managed under an Agency account. To work with Agency client accounts, [run the MCP server locally](./run-locally.mdx) with Docker and authenticate it with the appropriate Woodpecker API key. --- ## Connect ChatGPT and Codex to the Woodpecker MCP server Connect Woodpecker to ChatGPT or Codex in just a few steps - no local server or API key required. Once connected, you can chat with ChatGPT or Codex to manage your campaigns, prospects, and email sequences. ## Connecting ChatGPT and Codex Choose whether you want to connect through the ChatGPT desktop app or Codex CLI. First add the Woodpecker MCP server, then authorize access to your Woodpecker account. 1. In the ChatGPT desktop app, open **Settings** and select **Plugins**. 2. Select **Add** in the top-right corner, then **Add MCP Server**. 3. Enter a name for the server, for example `woodpecker`. 4. Set **Type** to **Streamable HTTP** and enter the following **URL**: ```text https://api.woodpecker.co/mcp/v1 ``` 5. Select **Save**. 6. Open the **MCPs** tab and select **Authenticate** for the Woodpecker MCP server. 7. Sign in to your Woodpecker account when prompted. 8. Review the requested access and select **Allow**. After authorization, ChatGPT can use the available Woodpecker [tools](./available-tools.mdx). To verify the connection, ask ChatGPT: ```text "What Woodpecker tools do you have access to?" "List my Woodpecker campaigns" ``` Add the Woodpecker remote MCP server using the Codex CLI: ```bash codex mcp add woodpecker --url https://api.woodpecker.co/mcp/v1 ``` Complete the authorization if prompted. If the authorization flow does not start, run: ```bash codex mcp login woodpecker ``` To check that the server has been added, run: ```bash codex mcp list ``` Start a Codex session and run `/mcp` to check the available MCP tools. To verify access to your Woodpecker account, ask Codex: ```text "List my Woodpecker campaigns" ``` See the official [Codex MCP documentation](https://developers.openai.com/codex/mcp) for additional configuration options. ## Troubleshoot the ChatGPT or Codex connection Authorization was not completed **Symptoms**: The Woodpecker MCP server has been added, but the app still asks you to authenticate. * In ChatGPT Desktop, open **Settings** → **Plugins** → **MCPs** and select **Authenticate** for Woodpecker * In Codex CLI, run `codex mcp login woodpecker` * Sign in to the intended Woodpecker account and complete the authorization by selecting **Allow**. Saving the server configuration alone does not authorize access to your account. The app cannot connect to the MCP server **Symptoms**: The app reports a connection error when adding or using the Woodpecker MCP server. * Check that the server URL is exactly `https://api.woodpecker.co/mcp/v1` * In ChatGPT Desktop, make sure **Type** is set to **Streamable HTTP** * In Codex CLI, use `codex mcp list` to check the configured server URL An Agency client account cannot be selected **Symptoms**: You connected an Agency account but cannot access or switch to one of its client accounts. * The hosted Woodpecker MCP server does not currently support selecting client accounts managed under an Agency account. To work with Agency client accounts, [run the MCP server locally](./run-locally.mdx) with Docker and authenticate it with the appropriate Woodpecker API key. --- ## Examples & tips Use these example prompts and tips to manage campaigns, add prospects, and review performance through the Woodpecker MCP server. Before using a prompt, replace the sample IDs and prospect details with data from your Woodpecker account. ## Examples #### Creating your first campaign ``` "Create a new email campaign with these details: - Name: Product Demo Outreach - Subject: Quick demo of our new feature - Message: Hi {{FIRST_NAME}}, I'd love to show {{COMPANY}} our new feature. Are you available for a 15-minute demo? - Send Monday-Friday, 9 AM to 5 PM - Daily limit: 25 prospects - Use my main email account" ``` #### Adding Prospects in Bulk You can paste prospect data to the agent, or upload a csv file directly ``` "Add these prospects to campaign 12345: 1. John Doe, john@example.com, Example Corp, Marketing Director 2. Jane Smith, jane@example.com, Tech Solutions, CEO 3. Bob Johnson, bob@example.com, Startup Inc, CTO Use this personalization for each: - John: 'I saw your recent blog post about email marketing' - Jane: 'Congratulations on your recent funding round' - Bob: 'Your product launch looked impressive'" ``` #### Campaign Management ``` "Pause campaign 12345 and show me its performance statistics" "Update campaign 'Product Demo Outreach' to send only 15 prospects per day" "Add a follow-up email to campaign 12345 that sends 3 days after the first email" ``` #### Analytics and Reporting ``` "Show me statistics for all my running campaigns" "Which campaign has the highest open rate?" "Create a summary report of my campaign performance this month" ``` ## Tips #### Campaign Management * Start Small: Test with 5-10 prospects before scaling * Monitor Performance: Check statistics regularly * A/B Testing: Use multiple email versions for optimization #### Prospect Data * Personalization: Use meaningful snippets and custom fields * Segmentation: Organize prospects with tags and industries * Compliance: Include unsubscribe links and respect opt-outs #### AI Agent Interactions * Be Specific: Provide clear instructions for campaign creation * Verify Results: Check campaign details before running * Use Examples: Include sample content and personalization * Monitor Automation: Review AI-generated campaigns before deployment --- ## Woodpecker MCP Server The Woodpecker MCP (Model Context Protocol) server lets AI agents interact directly with your Woodpecker account. **Connect Claude, ChatGPT, or Codex** and other compatible AI agents directly with your Woodpecker account. Use natural language to manage campaigns, prospects, email sequences, mailboxes, and campaign performance without calling the Woodpecker API manually. You can start with straightforward tasks, such as listing campaigns or adding prospects, and move on to more advanced workflows, including multistep sequences, A/B testing, campaign configuration, and performance analysis. ## Features * Campaign Management: Create, update, run, pause and delete email campaigns * Prospect Operations: Add prospects to your account and campaigns, update and delete prospect data, search prospects * Email Composition: Create multistep email sequences with A/B testing capabilities * Analytics & Reporting: Retrieve campaign statistics and performance metrics * Mailbox Integration: Assign email accounts to campaigns * Advanced Configuration: Support for delivery schedules, timezone settings and GDPR compliance ## Prerequisites Choose the connection method that matches your AI agent. **We recommend connecting Claude, Claude Code, ChatGPT Desktop, or Codex CLI directly to the MCP server hosted by Woodpecker**. This setup requires no local server or API key. Alternatively, use Docker image if you want to use another compatible AI tool or MCP client, host the server yourself, or manage client accounts under a Woodpecker Agency account. Both connection methods provide access to the same Woodpecker tools. The hosted connection uses an authorization flow to grant access to your Woodpecker account, while the Docker setup works with MCP clients that support `stdio`. | Connection method | Supported AI agents | Requirements | |---|---|---| | **[Connect Claude](/docs/mcp/connect-claude)**Hosted by Woodpecker - Recommended | Claude (web, desktop, and mobile) and Claude Code | A Woodpecker account and [Claude](https://claude.ai/download) | | **[Connect ChatGPT & Codex](/docs/mcp/connect-openai)**Hosted by Woodpecker - Recommended | ChatGPT Desktop and Codex CLI | A Woodpecker account and the ChatGPT desktop app or Codex CLI | | **[Run locally with Docker](/docs/mcp/run-locally)** | Claude Desktop, Cursor, or another MCP client that supports `stdio` | A Woodpecker account with the API & Integration add-on enabled, a [Woodpecker API key](https://app.woodpecker.co/panel#add-ons/integrations/api-keys), and [Docker](https://docs.docker.com/desktop/) | --- ## Run the Woodpecker MCP server locally Run the Woodpecker MCP server with Docker if you want to host it yourself, connect it to an MCP client that supports `stdio`, or use it with client accounts managed under a Woodpecker Agency account. This setup requires: * A Woodpecker account with the API & Integration add-on enabled * A [Woodpecker API key](https://app.woodpecker.co/panel#add-ons/integrations/api-keys) * [Docker](https://docs.docker.com/desktop/) You can use the Docker server with Claude Desktop, Cursor, Gemini CLI, or another MCP client that supports the `stdio` transport. ## Start Docker 1. Install and run Docker. 2. Optional: pull the [MCP Docker image](https://hub.docker.com/r/woodpeckerco/woodpecker-mcp-server). The image will be pulled automatically when you start the AI agent. To ensure it is available ahead of time or avoid a delay during the initial startup, pull it manually: ```bash docker pull woodpeckerco/woodpecker-mcp-server ``` ## Configure the AI agent ### Claude Desktop The following steps are specific to [Claude Desktop](https://claude.ai/download). 1. Locate the Claude Desktop configuration file: ```text macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json Linux: ~/.config/Claude/claude_desktop_config.json ``` 2. Add the MCP server configuration. Edit the configuration file to include the Woodpecker MCP server. Replace `{YOUR_API_KEY}` with your [Woodpecker API key](https://app.woodpecker.co/panel#add-ons/integrations/api-keys). ```json { "mcpServers": { "woodpecker": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "WOODPECKER_API_KEY", "woodpeckerco/woodpecker-mcp-server" ], "env": { "WOODPECKER_API_KEY": "{YOUR_API_KEY}" } } } } ``` 3. Restart Claude Desktop if it was running. 4. Verify and test the connection. Look for the Woodpecker tools in the interface, or ask the AI agent: ```text "What Woodpecker tools do you have access to?" "List my Woodpecker campaigns" ``` ### Other AI agents * Ensure MCP support: Verify that your AI agent supports the Model Context Protocol * Authentication: Provide Woodpecker API credentials (`WOODPECKER_API_KEY`) through environment variables * Protocol: Use the `stdio` transport supported by your agent ## Troubleshooting Docker server not connecting **Symptoms**: AI agent can't access Woodpecker tools * Check if the Docker container is running: `docker ps` * Try the absolute Docker path (for example, `/usr/local/bin/docker`) in the MCP server configuration * Test the API key by making a direct [API call to Woodpecker](/docs/getting-started/authentication.mdx#authenticating-requests) Invalid API credentials **Symptoms**: "Unauthorized" or "Invalid API key" errors * Regenerate the API key in the [Woodpecker add-ons section](https://app.woodpecker.co/panel#add-ons/integrations/api-keys) * Update the environment variable with the new key * Restart the AI agent application Tools not available **Symptoms**: AI agent doesn't see Woodpecker functions * Check the MCP server configuration in the AI agent * Restart the AI agent application * Check the AI agent logs for MCP connection errors --- ## Woodpecker CLI The Woodpecker CLI brings your Woodpecker workspace to the terminal. It is useful when you want to check something quickly, automate a repeated task, or give an AI coding agent a clean way to work with Woodpecker from the shell. Command-line tools are a natural fit for API work: they are fast, scriptable and easy for tools like Codex or Claude Code to run. Use this section to get started, then use the [npm package page](https://www.npmjs.com/package/@woodpecker.co/cli) when you need the full command reference. ## What you can do Use the CLI to inspect and manage Woodpecker from your terminal, without clicking through the app for every check or operation. Common areas include: * managing account data such as campaigns, prospects, inbox messages, mailboxes, reports, webhooks, blacklists and agency resources * human-readable terminal output for manual checks * JSON output for scripts, CI jobs and AI agents * interactive selectors and prompts when running in a terminal * raw API requests when you need to call a Woodpecker endpoint directly ## Prerequisites Before using the CLI, make sure you have: * a Woodpecker account with API access enabled and an API key from [the add-ons section](https://app.woodpecker.co/panel#add-ons/integrations/api-keys) * Node.js 22 LTS, which is the recommended version For the exact supported Node.js range, installation details, command list, flags and behavior notes, see the [npm package README](https://www.npmjs.com/package/@woodpecker.co/cli). ## Next steps Start with [installation and setup](/docs/cli/installation.mdx), then try [examples & tips](/docs/cli/examples-tips.mdx) for manual checks, recurring scripts and AI-agent workflows. For complete command syntax, flags, output modes, security notes, retry behavior and shell completion, use the [npm package README](https://www.npmjs.com/package/@woodpecker.co/cli). --- ## Examples & tips(Cli) The CLI is useful when you want Woodpecker data close to where you already work: in a terminal, a script, or an AI coding agent. You can check campaign state, add entries to a blacklist, generate reports, fetch replies, or ask an agent to inspect account data and summarize what needs attention. If you work with agency accounts, you can also scope account-level commands to a client company with `--company-id`. These examples are meant to show practical patterns. For the full command list, flags and output details, see the [npm package README](https://www.npmjs.com/package/@woodpecker.co/cli). :::tip Use `--json` or `--output json` for deterministic output in scripts and AI-agent workflows. Human output is better for manual terminal checks, but JSON output is easier to parse. ::: ## Manual terminal checks Use the CLI when you need a quick answer and do not want to navigate through the UI. List running and paused campaigns: ```bash woodpecker campaigns list --status RUNNING,PAUSED ``` Check campaign details by ID: ```bash woodpecker campaigns get 1406957 ``` Add an email address to the account blacklist: ```bash woodpecker blacklist emails add --emails blacklisted@mail.com ``` Run an account-level command for a specific client company: ```bash woodpecker --company-id 123 campaigns list --status RUNNING ``` ## Recurring scripts This example generates a campaigns report for the previous 30 days, waits for it to be ready and saves the result to a local JSON file: ```bash #!/usr/bin/env bash set -euo pipefail : "${WOODPECKER_API_KEY:?Set WOODPECKER_API_KEY first}" report_hash="$( woodpecker --json reports generate campaigns --last-30-days | jq -r '.hash' )" woodpecker --json reports poll "$report_hash" --attempts 10 --interval-ms 2000 > campaign-report.json jq -r '.status' campaign-report.json ``` Save it as `campaign-report.sh`, then run it manually first: ```bash export WOODPECKER_API_KEY="{YOUR_API_KEY}" chmod +x campaign-report.sh ./campaign-report.sh ``` If the script prints `READY` and creates `campaign-report.json`, it works. After that, run it from your scheduler of choice, such as cron, GitHub Actions or another CI job. For scheduled jobs, load `WOODPECKER_API_KEY` from your platform secret manager instead of storing the key in the script. ## AI-agent workflows CLI commands are useful when an AI coding agent can run shell commands in your workspace. The agent can ask Woodpecker for current data, parse JSON, and then help you decide what to do next. Example prompts: ```text Use the Woodpecker CLI to fetch my latest inbox replies. Summarize who replied, and point out replies that I should prioritize first. ``` ```text Use the Woodpecker CLI to generate a campaigns report for the last 30 days, wait until the report is ready, and summarize the main campaign performance patterns. ``` --- ## Installation Install the Woodpecker CLI from npm, verify that it runs, and configure authentication before making API calls. For regular use, the global install is the simplest option because it gives you the short `woodpecker` command everywhere. If you cannot install global npm packages on your machine, use `npx` instead. Both options run the same CLI. ## Install ### Global install Install the CLI globally: ```bash npm install -g @woodpecker.co/cli ``` Then verify the installation: ```bash woodpecker --help woodpecker --version ``` If npm fails with a permissions error, your user probably cannot write to the global npm directory. If this is your own development machine and your setup allows it, you can run the install with `sudo`: ```bash sudo npm install -g @woodpecker.co/cli ``` On managed machines, CI environments or shared systems, using `npx` is often the cleaner option. ### Run with npx Use `npx` when you do not want, or cannot use, a global install: ```bash npx @woodpecker.co/cli --help ``` When using `npx`, the command prefix changes. Replace `woodpecker` with `npx @woodpecker.co/cli` in examples: ```bash npx @woodpecker.co/cli campaigns list ``` For one-off checks this is usually enough. For frequent terminal work or scripts, a global install is shorter and easier to read. ## Authentication For local terminal use, store your API key with: ```bash woodpecker login YOUR_API_KEY ``` You can inspect the active configuration with: ```bash woodpecker config show ``` For scripts, CI jobs or AI agents, prefer environment variables so secrets stay outside command history and project files: ```bash export WOODPECKER_API_KEY="{YOUR_API_KEY}" woodpecker campaigns list ``` If you use `npx`, the same commands work with the longer prefix: ```bash npx @woodpecker.co/cli login YOUR_API_KEY npx @woodpecker.co/cli campaigns list ``` ## Update If you installed the CLI globally, run the install command again to update to the latest published version: ```bash npm install -g @woodpecker.co/cli@latest ``` If you use `npx`, use the package name with `@latest` when you explicitly want the newest published version: ```bash npx @woodpecker.co/cli@latest --version ``` ## Further reference For exact Node.js support, all configuration options, environment variables, command flags and shell completion, see the [npm package README](https://www.npmjs.com/package/@woodpecker.co/cli).