curl --request POST \
--url https://api.goyappr.com/campaigns/{id}/leads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"lead_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"phone_numbers": [
"+972501234567"
]
}
'import requests
url = "https://api.goyappr.com/campaigns/{id}/leads"
payload = {
"lead_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"],
"phone_numbers": ["+972501234567"]
}
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({
lead_ids: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
phone_numbers: ['+972501234567']
})
};
fetch('https://api.goyappr.com/campaigns/{id}/leads', 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://api.goyappr.com/campaigns/{id}/leads",
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([
'lead_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
],
'phone_numbers' => [
'+972501234567'
]
]),
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://api.goyappr.com/campaigns/{id}/leads"
payload := strings.NewReader("{\n \"lead_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"phone_numbers\": [\n \"+972501234567\"\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://api.goyappr.com/campaigns/{id}/leads")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"lead_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"phone_numbers\": [\n \"+972501234567\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.goyappr.com/campaigns/{id}/leads")
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 \"lead_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"phone_numbers\": [\n \"+972501234567\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"campaign_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"enrolled": 123,
"already_enrolled": 123,
"leads_created": 123,
"leads_matched": 123,
"invalid_phone": [
{
"lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"phone": "<string>",
"error": "<string>"
}
],
"on_do_not_call": [
"+972501234567"
],
"not_found": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"total_leads": 123,
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}{
"error": "<string>",
"code": "<string>"
}{
"error": "<string>",
"code": "<string>"
}{
"error": "ALREADY_IN_ACTIVE_CAMPAIGN",
"message": "<string>",
"campaign_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"enrolled": 123,
"already_enrolled": 123,
"leads_created": 123,
"leads_matched": 123,
"invalid_phone": [
{
"lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"phone": "<string>",
"error": "<string>"
}
],
"on_do_not_call": [
"+972501234567"
],
"not_found": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"total_leads": 123,
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}Enroll contacts
Enroll existing leads by lead_ids, raw numbers via phone_numbers, or both in
one request. Raw numbers are normalized to E.164 and matched against your existing
leads; a lead is created only when there is no match. Required scope
campaigns:manage.
Enrollment is per-row and idempotent: unparseable numbers, numbers on the do-not-call list, and unknown lead IDs are reported in the response instead of failing the batch, and re-enrolling an existing contact is a no-op. At most 1,000 contacts per request — send several requests for larger lists.
You can enroll into a running campaign; new contacts join the pacing queue. A
completed, stopped or archived campaign rejects enrollment.
curl --request POST \
--url https://api.goyappr.com/campaigns/{id}/leads \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"lead_ids": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"phone_numbers": [
"+972501234567"
]
}
'import requests
url = "https://api.goyappr.com/campaigns/{id}/leads"
payload = {
"lead_ids": ["3c90c3cc-0d44-4b50-8888-8dd25736052a"],
"phone_numbers": ["+972501234567"]
}
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({
lead_ids: ['3c90c3cc-0d44-4b50-8888-8dd25736052a'],
phone_numbers: ['+972501234567']
})
};
fetch('https://api.goyappr.com/campaigns/{id}/leads', 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://api.goyappr.com/campaigns/{id}/leads",
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([
'lead_ids' => [
'3c90c3cc-0d44-4b50-8888-8dd25736052a'
],
'phone_numbers' => [
'+972501234567'
]
]),
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://api.goyappr.com/campaigns/{id}/leads"
payload := strings.NewReader("{\n \"lead_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"phone_numbers\": [\n \"+972501234567\"\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://api.goyappr.com/campaigns/{id}/leads")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"lead_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"phone_numbers\": [\n \"+972501234567\"\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.goyappr.com/campaigns/{id}/leads")
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 \"lead_ids\": [\n \"3c90c3cc-0d44-4b50-8888-8dd25736052a\"\n ],\n \"phone_numbers\": [\n \"+972501234567\"\n ]\n}"
response = http.request(request)
puts response.read_body{
"campaign_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"enrolled": 123,
"already_enrolled": 123,
"leads_created": 123,
"leads_matched": 123,
"invalid_phone": [
{
"lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"phone": "<string>",
"error": "<string>"
}
],
"on_do_not_call": [
"+972501234567"
],
"not_found": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"total_leads": 123,
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}{
"error": "<string>",
"code": "<string>"
}{
"error": "<string>",
"code": "<string>"
}{
"error": "ALREADY_IN_ACTIVE_CAMPAIGN",
"message": "<string>",
"campaign_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"enrolled": 123,
"already_enrolled": 123,
"leads_created": 123,
"leads_matched": 123,
"invalid_phone": [
{
"lead_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"phone": "<string>",
"error": "<string>"
}
],
"on_do_not_call": [
"+972501234567"
],
"not_found": [
"3c90c3cc-0d44-4b50-8888-8dd25736052a"
],
"total_leads": 123,
"company_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a"
}lead_ids, raw phone numbers with phone_numbers, or both in one request. Numbers are matched to existing leads where possible and created as new leads otherwise.
The response is a report, not a failure: bad rows are described rather than aborting the batch, so a list with three unparseable numbers and two people on the do-not-call list still enrolls everybody else and tells you exactly who was skipped and why.
Enrollment works on a draft campaign and on a live one — adding contacts to a running campaign is supported and they join the queue at the normal pace.
Required scope: campaigns:manage.
Path parameters
| Parameter | Type | Notes |
|---|---|---|
id | uuid | Campaign ID. Must belong to the API key’s workspace. |
Body
At least one oflead_ids / phone_numbers must be present and non-empty. Their combined length may not exceed 1000 per request — send several requests for a longer list.
| Field | Type | Notes |
|---|---|---|
lead_ids | uuid[] | IDs of existing leads (GET /leads). IDs that do not exist in your workspace, or are deleted, come back in not_found. |
phone_numbers | array | Each entry is either a bare phone string, or an object (below). |
phone_numbers[].phone | string | Required in object form. Any common format — +972501234567, 972501234567, 0501234567 — normalized to E.164 before anything is written. |
phone_numbers[].name | string | Optional. Used when a new lead is created. |
phone_numbers[].email | string | Optional. Used when a new lead is created. |
phone_numbers[].notes | string | Optional, truncated to 2000 characters. Becomes the lead’s long-term context, which the agent can draw on during the call. |
0501234567 in one row and +972501234567 in another resolves to one lead and one enrollment rather than two.
name, email and notes apply to newly created leads. When a number matches a lead you already have, the existing lead is enrolled as-is and its details are left untouched — update it through PATCH /leads/{id} if you need to change them.
What gets skipped
| Reason | Reported as | Behaviour |
|---|---|---|
| Unparseable phone number | invalid_phone | Skipped. Includes the offending input, and the lead_id when the bad number came from an existing lead. |
| Lead ID not in this workspace | not_found | Skipped. |
| Number on the do-not-call list | on_do_not_call | Skipped, and reported up front rather than surfacing later as blocked calls. Scope-limited entries are honoured against this campaign’s agent. |
| Already enrolled in this campaign | already_enrolled | No duplicate row, no error. Re-sending the same list is safe. |
| Already live in a different campaign | 409 ALREADY_IN_ACTIVE_CAMPAIGN | A number can only be dialed by one active campaign at a time — otherwise the same person receives two campaigns’ worth of attempts. Stop or finish the other campaign, or exclude the contact there, then retry. |
Response fields
| Field | Type | Meaning |
|---|---|---|
campaign_id | uuid | Echo of the path parameter. |
enrolled | integer | Contacts newly added to this campaign. |
already_enrolled | integer | Contacts that were already on this campaign. |
leads_created | integer | New leads created from phone_numbers. |
leads_matched | integer | phone_numbers entries that matched a lead you already had. |
invalid_phone | array | Rows whose number could not be parsed. |
on_do_not_call | string[] | E.164 numbers skipped because they are suppressed. |
not_found | string[] | lead_ids that do not resolve in this workspace. |
total_leads | integer | Total contacts on the campaign after this request. Omitted when nothing was enrolled. |
company_id | uuid | Workspace this response belongs to. |
Example request
curl -X POST "https://api.goyappr.com/campaigns/CAMPAIGN_ID/leads" \
-H "Authorization: Bearer $YAPPR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"lead_ids": [
"a1c3e5f7-0000-4b21-9c30-000000000021",
"a1c3e5f7-0000-4b21-9c30-000000000022"
],
"phone_numbers": [
{ "phone": "0501234567", "name": "Dana Levi", "email": "dana@example.com", "notes": "Renewal due in August. Prefers mornings." },
{ "phone": "+972521112222", "name": "Yossi Mor" },
"0539998888"
]
}'
Example response
{
"campaign_id": "b3f1c0d2-5a44-4f0e-9c11-7a2e8d3f0001",
"enrolled": 4,
"already_enrolled": 1,
"leads_created": 2,
"leads_matched": 1,
"invalid_phone": [],
"on_do_not_call": ["+972539998888"],
"not_found": [],
"total_leads": 412,
"company_id": "fe493f11-0000-0000-0000-000000000001"
}
total_leads:
{
"campaign_id": "b3f1c0d2-5a44-4f0e-9c11-7a2e8d3f0001",
"enrolled": 0,
"already_enrolled": 0,
"leads_created": 0,
"leads_matched": 0,
"invalid_phone": [{ "phone": "not-a-number" }],
"on_do_not_call": [],
"not_found": ["a1c3e5f7-0000-4b21-9c30-0000000000ff"],
"company_id": "fe493f11-0000-0000-0000-000000000001"
}
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | — | Neither lead_ids nor phone_numbers supplied (or both empty). |
| 400 | — | More than 1000 contacts in one request. |
| 400 | — | Campaign is completed, stopped or archived — those cannot take new contacts. |
| 400 | — | Request body is not valid JSON. |
| 401 | INSUFFICIENT_SCOPE | API key lacks campaigns:manage. |
| 404 | — | No campaign with that ID in this workspace, or it has been archived. |
| 409 | ALREADY_IN_ACTIVE_CAMPAIGN | One or more numbers are live in another active campaign. The body carries the same report fields, so you can see how far the request got. |
Authorizations
Your Yappr API key (e.g. ypr_live_...). Generate one in the dashboard under Settings → API Keys.
Path Parameters
Body
Supply lead_ids, phone_numbers, or both — at least one must be non-empty.
Response
Enrollment report. Returned even when nothing was enrolled — read the counters and the rejection arrays to see why.
Result of POST /campaigns/{id}/leads. Enrollment is per-row and
best-effort: a bad phone number or a number on the do-not-call list is
reported here rather than failing the whole batch, and re-enrolling an
existing contact is idempotent.
Numbers are canonicalized to E.164 before anything is written, so the same person cannot be enrolled twice under two formats.
Contacts newly enrolled by this request.
Contacts that were already enrolled — no-ops, not errors.
New leads created from phone_numbers entries with no existing match.
phone_numbers entries matched to a lead you already had.
Rows rejected before enrollment because the number could not be parsed, or the lead could not be created.
Show child attributes
Show child attributes
Numbers excluded because they are on the workspace do-not-call list. A verbal opt-out on a call adds the number to that list automatically, workspace-wide, so previously-contacted people can appear here.
IDs from lead_ids that are not leads in this workspace.
Total contacts enrolled in the campaign after this request. Omitted when nothing was enrolled.