curl --request POST \
--url https://app.manypi.com/api/outreach/enroll \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"campaign_id": "…",
"lead_ids": [
"8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d"
]
}
'import requests
url = "https://app.manypi.com/api/outreach/enroll"
payload = {
"campaign_id": "…",
"lead_ids": ["8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({campaign_id: '…', lead_ids: ['8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d']})
};
fetch('https://app.manypi.com/api/outreach/enroll', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.manypi.com/api/outreach/enroll",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'campaign_id' => '…',
'lead_ids' => [
'8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.manypi.com/api/outreach/enroll"
payload := strings.NewReader("{\n \"campaign_id\": \"…\",\n \"lead_ids\": [\n \"8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.manypi.com/api/outreach/enroll")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"campaign_id\": \"…\",\n \"lead_ids\": [\n \"8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.manypi.com/api/outreach/enroll")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"campaign_id\": \"…\",\n \"lead_ids\": [\n \"8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"enrolled": 214,
"requested": 240,
"already_enrolled": 9,
"skipped_no_email": 12,
"skipped_unsubscribed": 3,
"skipped_bounced": 2,
"queued_for_validation": 4,
"summary": "Added 214 of 240 leads. 9 leads are already in this campaign. 12 leads have no email address yet, so there is nothing to send to. Find their address first, then add them again. 3 leads unsubscribed, so they stay on your do-not-contact list. Remove them from Suppressions if that was a mistake. 2 leads bounced the last time you wrote. Sending again would only damage the reputation of the inbox you send from."
}{
"error": "<string>",
"code": "<string>",
"details": "<unknown>"
}{
"error": "<string>",
"code": "<string>",
"details": "<unknown>"
}{
"error": "<string>",
"code": "<string>",
"details": "<unknown>"
}Enroll leads into a campaign
Queue the campaign’s sequence for real sending, respecting the inbox’s warmup and daily caps. Up to 1,000 leads per call; ids past the first 1,000 are counted in skipped_over_limit.
Skipped rather than failed: leads with no email, leads already enrolled, unsubscribed or suppressed addresses, addresses that bounced, addresses known to be undeliverable, and — when the brand only writes to confirmed addresses — addresses that could not be confirmed. Each cause has its own counter; summary explains them in plain language.
When the brand has validate on enroll turned on (the default), unchecked addresses are enrolled and checked in the background (queued_for_validation). The result is applied at send time, so an address that turns out undeliverable is never sent to.
Sending cannot be undone. Requires a paid plan and the outreach:send permission.
curl --request POST \
--url https://app.manypi.com/api/outreach/enroll \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"campaign_id": "…",
"lead_ids": [
"8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d"
]
}
'import requests
url = "https://app.manypi.com/api/outreach/enroll"
payload = {
"campaign_id": "…",
"lead_ids": ["8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d"]
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({campaign_id: '…', lead_ids: ['8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d']})
};
fetch('https://app.manypi.com/api/outreach/enroll', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.manypi.com/api/outreach/enroll",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'campaign_id' => '…',
'lead_ids' => [
'8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.manypi.com/api/outreach/enroll"
payload := strings.NewReader("{\n \"campaign_id\": \"…\",\n \"lead_ids\": [\n \"8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d\"\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.manypi.com/api/outreach/enroll")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"campaign_id\": \"…\",\n \"lead_ids\": [\n \"8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.manypi.com/api/outreach/enroll")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"campaign_id\": \"…\",\n \"lead_ids\": [\n \"8f1c0e6a-1c3e-4f2a-9a4b-0f7a1e2b3c4d\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"enrolled": 214,
"requested": 240,
"already_enrolled": 9,
"skipped_no_email": 12,
"skipped_unsubscribed": 3,
"skipped_bounced": 2,
"queued_for_validation": 4,
"summary": "Added 214 of 240 leads. 9 leads are already in this campaign. 12 leads have no email address yet, so there is nothing to send to. Find their address first, then add them again. 3 leads unsubscribed, so they stay on your do-not-contact list. Remove them from Suppressions if that was a mistake. 2 leads bounced the last time you wrote. Sending again would only damage the reputation of the inbox you send from."
}{
"error": "<string>",
"code": "<string>",
"details": "<unknown>"
}{
"error": "<string>",
"code": "<string>",
"details": "<unknown>"
}{
"error": "<string>",
"code": "<string>",
"details": "<unknown>"
}Authorizations
A ManyPI API key (mpi_...) from the profile menu → API Access in the dashboard, or an OAuth 2.1 access token obtained through the MCP consent flow.
Body
Response
Enrollment outcome.
Skips are reported as one counter per cause rather than a single total. Counters for causes that did not occur may be omitted. For the number of leads that were not enrolled, add up every skipped_* counter and already_enrolled — or compute requested - enrolled.
Leads that now have an enrollment they did not have before. Includes queued_for_validation.
Lead ids in the request, before any skipping.
Leads already enrolled in this campaign.
Ids that match no lead this workspace can see.
Leads with no email address.
Leads who unsubscribed or are on the do-not-contact list.
Leads whose address bounced the last time you wrote.
Addresses known to be dead: invalid, disposable, or a domain that takes no mail.
Addresses the mail server would not confirm, skipped because the brand only writes to confirmed addresses.
Ids past the first 1,000 in this request.
Enrolled leads whose unchecked address is being verified in the background, when the brand has validate-on-enroll turned on.
What happened, in plain language — the same text the dashboard shows.
