Upsert Recipients
curl --request PUT \
--url https://api.example.com/email/lists/{list_id}/recipients \
--header 'Content-Type: application/json' \
--data '
{
"recipients": [
{
"email": "<string>",
"name": "<string>",
"external_id": "<string>",
"attributes": {}
}
]
}
'import requests
url = "https://api.example.com/email/lists/{list_id}/recipients"
payload = { "recipients": [
{
"email": "<string>",
"name": "<string>",
"external_id": "<string>",
"attributes": {}
}
] }
headers = {"Content-Type": "application/json"}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
recipients: [{email: '<string>', name: '<string>', external_id: '<string>', attributes: {}}]
})
};
fetch('https://api.example.com/email/lists/{list_id}/recipients', 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.example.com/email/lists/{list_id}/recipients",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'recipients' => [
[
'email' => '<string>',
'name' => '<string>',
'external_id' => '<string>',
'attributes' => [
]
]
]
]),
CURLOPT_HTTPHEADER => [
"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.example.com/email/lists/{list_id}/recipients"
payload := strings.NewReader("{\n \"recipients\": [\n {\n \"email\": \"<string>\",\n \"name\": \"<string>\",\n \"external_id\": \"<string>\",\n \"attributes\": {}\n }\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
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.put("https://api.example.com/email/lists/{list_id}/recipients")
.header("Content-Type", "application/json")
.body("{\n \"recipients\": [\n {\n \"email\": \"<string>\",\n \"name\": \"<string>\",\n \"external_id\": \"<string>\",\n \"attributes\": {}\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/email/lists/{list_id}/recipients")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"recipients\": [\n {\n \"email\": \"<string>\",\n \"name\": \"<string>\",\n \"external_id\": \"<string>\",\n \"attributes\": {}\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"added": 123,
"updated": 123,
"reactivated": 123,
"unchanged": 123,
"skipped_unsubscribed": 123,
"invalid": [
"<string>"
],
"conflicts": [
{}
]
}Mailing Lists
Upsert Recipients
Idempotent add-or-update — the call to sync your user base against
PUT
/
email
/
lists
/
{list_id}
/
recipients
Upsert Recipients
curl --request PUT \
--url https://api.example.com/email/lists/{list_id}/recipients \
--header 'Content-Type: application/json' \
--data '
{
"recipients": [
{
"email": "<string>",
"name": "<string>",
"external_id": "<string>",
"attributes": {}
}
]
}
'import requests
url = "https://api.example.com/email/lists/{list_id}/recipients"
payload = { "recipients": [
{
"email": "<string>",
"name": "<string>",
"external_id": "<string>",
"attributes": {}
}
] }
headers = {"Content-Type": "application/json"}
response = requests.put(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
recipients: [{email: '<string>', name: '<string>', external_id: '<string>', attributes: {}}]
})
};
fetch('https://api.example.com/email/lists/{list_id}/recipients', 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.example.com/email/lists/{list_id}/recipients",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "PUT",
CURLOPT_POSTFIELDS => json_encode([
'recipients' => [
[
'email' => '<string>',
'name' => '<string>',
'external_id' => '<string>',
'attributes' => [
]
]
]
]),
CURLOPT_HTTPHEADER => [
"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.example.com/email/lists/{list_id}/recipients"
payload := strings.NewReader("{\n \"recipients\": [\n {\n \"email\": \"<string>\",\n \"name\": \"<string>\",\n \"external_id\": \"<string>\",\n \"attributes\": {}\n }\n ]\n}")
req, _ := http.NewRequest("PUT", url, payload)
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.put("https://api.example.com/email/lists/{list_id}/recipients")
.header("Content-Type", "application/json")
.body("{\n \"recipients\": [\n {\n \"email\": \"<string>\",\n \"name\": \"<string>\",\n \"external_id\": \"<string>\",\n \"attributes\": {}\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/email/lists/{list_id}/recipients")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Put.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"recipients\": [\n {\n \"email\": \"<string>\",\n \"name\": \"<string>\",\n \"external_id\": \"<string>\",\n \"attributes\": {}\n }\n ]\n}"
response = http.request(request)
puts response.read_body{
"added": 123,
"updated": 123,
"reactivated": 123,
"unchanged": 123,
"skipped_unsubscribed": 123,
"invalid": [
"<string>"
],
"conflicts": [
{}
]
}Adds or updates up to 1000 recipients per request. This is the endpoint a signup hook, nightly job or Zapier/n8n flow should call: sending the same payload twice is harmless, and sending a changed one updates the contact in place. See the sync guide for end-to-end recipes.
How each entry is matched and what happens:
| The entry… | Result |
|---|---|
has an external_id that is already on the list | That recipient is updated — including its email, if it changed |
has an email that is on the list with no external_id | That recipient is updated and adopts the external_id |
| matches nothing | Added as a new active recipient |
matches a removed recipient | Reactivated, then updated |
matches an unsubscribed recipient | Left completely untouched; counted in skipped_unsubscribed |
has an email that belongs to a different external_id | Reported in conflicts, nothing changed |
name and external_id are only written when you send them. attributes replaces the stored map when present. Upsert never deletes: remove departed users with Remove Recipient.
Authentication
This endpoint requires an API token passed as a Bearer token in theAuthorization header.
Authorization: Bearer YOUR_API_TOKEN
Path Parameters
string
required
The mailing list’s unique identifier (UUID).
Body
(string | object)[]
required
1–1000 entries. A plain string is an email address. An object has:
Show Recipient object
Show Recipient object
string
required
Email address (case-insensitive)
string
Display name, up to 200 characters. Used for the greeting in release emails.
string
Your own id for this person (user id, customer id), up to 255 characters, unique per list. Key by it so an email change updates the same recipient.
object
Up to 20 keys (≤ 64 chars each) with string, number, boolean or null values; at most 2 KB serialized. Replaces the stored map.
Request
curl -X PUT "https://api.shipstar.ai/api/v1/email/lists/a1b2c3d4-e5f6-7890-abcd-ef1234567890/recipients" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{"email": "[email protected]", "name": "Sam Lee", "external_id": "usr_42", "attributes": {"plan": "pro"}},
"[email protected]"
]
}'
const listId = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
const response = await fetch(`https://api.shipstar.ai/api/v1/email/lists/${listId}/recipients`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_API_TOKEN',
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipients: [
{ email: '[email protected]', name: 'Sam Lee', external_id: 'usr_42', attributes: { plan: 'pro' } },
'[email protected]'
]
})
});
const result = await response.json();
import requests
list_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
response = requests.put(
f'https://api.shipstar.ai/api/v1/email/lists/{list_id}/recipients',
headers={'Authorization': 'Bearer YOUR_API_TOKEN'},
json={'recipients': [
{'email': '[email protected]', 'name': 'Sam Lee', 'external_id': 'usr_42',
'attributes': {'plan': 'pro'}},
'[email protected]',
]},
)
result = response.json()
Response
integer
required
New recipients created
integer
required
Existing active recipients whose fields changed
integer
required
Removed recipients put back on the list
integer
required
Entries that matched an active recipient with identical fields
integer
required
Entries that matched a recipient who unsubscribed; nothing was changed
string[]
required
Entries whose email failed validation
object[]
required
Entries that could not be applied, each with
email, external_id and a reason.200
{
"added": 1,
"updated": 1,
"reactivated": 0,
"unchanged": 0,
"skipped_unsubscribed": 0,
"invalid": [],
"conflicts": []
}
Errors
| Status | Description |
|---|---|
| 400 | Applying the batch would exceed 500 active recipients (nothing is written; split the batch or remove contacts first) |
| 401 | Invalid or expired API token |
| 404 | Mailing list not found in the token’s project |
| 422 | Invalid request body (empty array, more than 1000 entries, oversize attributes) |
Rate Limits
This endpoint is limited to 30 requests per minute per IP.Was this page helpful?