Send Batch Campaign
api.ziett.co/c/v1 /campaigns/batch
Initiate a high-throughput messaging campaign for multiple recipients. Instead of making hundreds of individual HTTP requests, this endpoint allows you to submit a batch payload. The system will process the list, validate numbers, and queue them for delivery asynchronously.
One endpoint, multiple channels. You choose the channel with channel_type; the rest of the request shape stays the same.
For sending a single time-sensitive alert (like an OTP), use Send Message instead.
Choose your channel
| SMS | ||
|---|---|---|
channel_type | SMS | WHATSAPP |
| Message source | content (free text) or template_id | template_id (a template approved by Meta) |
| Variables | Optional, when using a template | Required whenever the template declares variables |
| Delivery | Carrier network | Meta Cloud API |
How It Works
Because batch processing handles hundreds of numbers simultaneously, this endpoint operates entirely asynchronously to prevent HTTP timeouts.
- Creation — The API immediately accepts your payload and creates a campaign record with a
PROCESSINGstatus. - Processing — Background workers parse the recipient list, format the numbers, resolve template variables for each recipient, calculate routing paths, and debit the total cost from your organization's billing account balance.
- Queueing — Individual message objects are generated and pushed to the provider specific delivery queues.
- Sending — Once all messages are successfully enqueued, the campaign status transitions to
SENDING. - Completion — Another system process will monitor the message delivery and mark the campaign as completed once all messages have been
SENT.
202 Acceptedis returned instantly after step 1. You should rely on webhooks to be notified when the campaign reaches theCOMPLETEDstate or track its progress via the Retrieve Campaign endpoint.
Request
Required scope: apikey:campaign:send
Body
Content-Type: application/json
| Field | Type | Required | Description |
|---|---|---|---|
remitter_id | uuid | ✅ | The approved Sender ID resource used to issue the campaign. For WhatsApp, the Ziett ID of the WhatsApp number linked to your organization. Rules: Must be a valid UUID v7. |
channel_type | enum | ✅ | The communication channel used to deliver the batch. Rules: Accepted values are SMS, WHATSAPP. |
recipients | array[string] | ✅ | List of destination phone numbers. Accepts local formats or international E.164 format. Rules: • Array length: 2 to 1000 items.• Regex format per item: ^\+?[\d\s\-\.]{7,30}$ |
content | string | ⚠️ | The text content of the message, for free-text SMS. Long messages are split automatically. Not used for WhatsApp. Rules: Minimum 1, Maximum 10,000 characters. |
template_id | uuid | ⚠️ | UUID of a template to send. Required for WhatsApp. For SMS, use it instead of content. |
variables | object | ❌ | Values for the template's variables. See Template Variables. |
name | string | ❌ | Internal friendly name for the campaign to help you identify it in the dashboard. Rules: Minimum 1, Maximum 200 characters. |
country_alpha2 | string | ❌ | ISO Code 3166-1 alpha-2 specifying the default country if local numbers are passed without international prefixes (e.g., AO, PT). Rules: Exactly 2 characters matching regex ^[A-Z]{2}$. |
⚠️ Provide either
contentortemplate_id, depending on the channel — see the channel sections below.
Need to reach a single recipient? A batch requires at least 2 recipients. Use Send Message instead.
Template Variables
When you send from a template, the variables object fills in its placeholders for every recipient. Values can be sent literally in values, read from each recipient's contact with mapping, or matched automatically by name.
The full rules (resolution order, contact lookup, key format per channel) are in Templates & Variables. One thing is specific to campaigns:
valuesare shared by the whole batch. A literal invaluesis sent to every recipient. To personalize each message (a name, an order, a balance), usemappingor name your template variables after your contact attributes so they are matched automatically. Each recipient is resolved against their own contact, so recipients must exist in your audience for contact data to be found.
Sending SMS
Set channel_type to SMS and provide the text with either:
content— free text, sent as-is to every recipient; ortemplate_id— a saved template, optionally withvariables.
Examples
import httpx
response = httpx.post(
"https://api.ziett.co/c/v1/campaigns/batch",
headers={
"X-API-KEY": "zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type": "application/json",
},
json={
"name": "Black Friday Promo - Luanda",
"remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
"channel_type": "SMS",
"country_alpha2": "AO",
"content": "Flash Sale! Enjoy 50% off on all items this weekend only.",
"recipients": [
"+244990000001",
"+244990000002",
"990000003", # Will be parsed using country_alpha2 'AO'
],
},
)
if response.status_code == 202:
print("Batch accepted. Processing asynchronously.")
else:
print(f"Error: {response.json()}")
curl -X POST https://api.ziett.co/c/v1/campaigns/batch \
-H "Content-Type: application/json" \
-H "X-API-KEY: zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-d '{
"name": "System Outage Notice",
"remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
"channel_type": "SMS",
"content": "Our servers will undergo maintenance at 2 AM WAT.",
"recipients": [
"+244990000001",
"+244990000002"
]
}'
const response = await fetch('https://api.ziett.co/c/v1/campaigns/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': 'zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
body: JSON.stringify({
name: 'VIP Members Invite',
remitter_id: '019b4b56-e704-7c30-83a0-527df63c3e00',
channel_type: 'SMS',
content: 'Exclusive VIP event this Saturday. Present this SMS at the door.',
recipients: ['+244990000001', '+244990000002', '+244990000003'],
}),
})
if (response.status === 202) {
console.log('Batch campaign accepted.')
} else {
const error = await response.json()
console.error('Validation failed:', error)
}
curl -X POST https://api.ziett.co/c/v1/campaigns/batch \
-H "Content-Type: application/json" \
-H "X-API-KEY: zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-d '{
"name": "Payment Reminder",
"remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
"template_id": "019c1a2e-5b10-7a44-9d3e-6f0b2c7a91d4",
"channel_type": "SMS",
"country_alpha2": "AO",
"recipients": ["+244990000001", "+244990000002"],
"variables": {
"values": { "due_date": "30 Oct" },
"mapping": {
"name": { "attribute": "first_name", "default": "customer" }
}
}
}'
Sending WhatsApp
Set channel_type to WHATSAPP and provide a template_id. WhatsApp messages are sent through Meta's Cloud API, so the content comes from a template approved by Meta; your request only supplies the values for its variables.
remitter_idfor WhatsApp. Use the Ziett ID of the WhatsApp number linked to your organization — the internal Ziett identifier, not the phone number ID from Meta.
Variable keys
Keys in values and mapping are written as <scope>:<name>, one scope per part of the template:
header:1orheader:<name>— header placeholders.body:1,body:2, … orbody:<name>— body placeholders.button:0,button:1, … — button parameters.
Buttons start at 0. Header and body placeholders are numbered from 1, but the first button is
button:0.
Use numbers for positional templates ({{1}}) and names for named templates ({{customer_name}}). See Templates & Variables for the full table.
Examples
Personalized per recipient. The body declares {{customer_name}} and {{promo_code}}. The promo code is the same for everyone, so it goes in values; the name differs for each recipient, so it is read from their contact:
curl -X POST https://api.ziett.co/c/v1/campaigns/batch \
-H "Content-Type: application/json" \
-H "X-API-KEY: zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-d '{
"name": "Black Friday - WhatsApp",
"remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
"template_id": "019c2f7d-8a31-7b02-b6c5-1e94d0a37f58",
"channel_type": "WHATSAPP",
"country_alpha2": "AO",
"recipients": ["+244990000001", "+244990000002", "990000003"],
"variables": {
"values": { "body:promo_code": "BF2026" },
"mapping": {
"body:customer_name": { "attribute": "first_name", "default": "customer" }
}
}
}'
import httpx
response = httpx.post(
"https://api.ziett.co/c/v1/campaigns/batch",
headers={"X-API-KEY": "zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
json={
"name": "Black Friday - WhatsApp",
"remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
"template_id": "019c2f7d-8a31-7b02-b6c5-1e94d0a37f58",
"channel_type": "WHATSAPP",
"country_alpha2": "AO",
"recipients": ["+244990000001", "+244990000002", "990000003"],
"variables": {
"values": {"body:promo_code": "BF2026"},
"mapping": {
"body:customer_name": {"attribute": "first_name", "default": "customer"}
},
},
},
)
print(response.status_code, response.json())
const response = await fetch('https://api.ziett.co/c/v1/campaigns/batch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-KEY': 'zk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx',
},
body: JSON.stringify({
name: 'Black Friday - WhatsApp',
remitter_id: '019b4b56-e704-7c30-83a0-527df63c3e00',
template_id: '019c2f7d-8a31-7b02-b6c5-1e94d0a37f58',
channel_type: 'WHATSAPP',
country_alpha2: 'AO',
recipients: ['+244990000001', '+244990000002', '990000003'],
variables: {
values: { 'body:promo_code': 'BF2026' },
mapping: {
'body:customer_name': { attribute: 'first_name', default: 'customer' },
},
},
}),
})
console.log(response.status, await response.json())
Template without variables. If the template has no placeholders, variables can be omitted:
{
"remitter_id": "019b4b56-e704-7c30-83a0-527df63c3e00",
"template_id": "019c4b11-3e72-7c90-a1d4-8f25b6c0e9a3",
"channel_type": "WHATSAPP",
"recipients": ["+244990000001", "+244990000002"]
}
Responses
202 Accepted — Campaign Queued
The payload schema is valid, and the campaign has been enqueued for background validation and processing. The response is the same for every channel.
{
"message": "Campaign accepted and is pending processing.",
"campaign_id": "019b4b56-e704-7c30-83a0-527df63c3e00"
}
(Note: Save the campaign_id returned to poll its status later or match it against incoming webhooks).
Errors
All errors share the same body shape: code, message, status, trace_id, timestamp and service. Include the trace_id when contacting support.
401 Unauthorized — Invalid API Key
The X-API-KEY header is missing, malformed, or the key has been revoked.
{
"code": "AUTH_INVALID_API_KEY",
"message": "The provided API key is invalid or has been revoked.",
"status": 401,
"trace_id": "a1b2c3d4e5f644358a9e7011c123c831",
"timestamp": "2026-05-14T10:30:00.000000+00:00",
"service": "core"
}
402 Payment Required — Insufficient Funds
Your organization's credit balance is strictly below the estimated cost required to dispatch this batch. The campaign is immediately aborted.
{
"code": "BILLING_INSUFFICIENT_FUNDS",
"message": "Your account balance is too low to process this batch request. Please top up your credits.",
"status": 402,
"trace_id": "c109f78ae57744358a9e7011c123c831",
"timestamp": "2026-05-14T10:30:00.000000+00:00",
"service": "billing"
}
422 Unprocessable Entity — Validation Error
The request body failed to meet constraints (e.g., array contains more than 1,000 numbers, or an item fails the regex pattern).
{
"code": "VALIDATION_ERROR",
"message": "One or more fields failed validation.",
"status": 422,
"trace_id": "f812a3bc91e044358a9e7011c456d901",
"timestamp": "2026-05-14T10:30:00.000000+00:00",
"service": "core",
"fields": {
"recipients": "Ensure this value has at most 1000 items.",
"country_alpha2": "String should match pattern '^[A-Z]{2}$'."
}
}
Rate Limits
Due to the heavy database operations required to process batches, this endpoint is strictly limited to 10 requests per minute per API Key, shared across channels. Ensure your systems employ proper backoff strategies. See Rate Limits & Reliability.
Related Endpoints
- Send Message — Dispatch a single transactional message instantly.
- Retrieve Campaign — Retrieve the execution status and analytics from a campaign.
- Templates & Variables — How template variables are resolved.