Skip to navigation

Campaigns

Run one agent against a list of contacts, on one channel, inside a schedule you control.
View as Markdown

A campaign binds an agent to a list of contacts and a goal. On the voice channel, activating a campaign queues a call for every pending contact and dials them within the campaign’s schedule, retrying people who did not answer. On whatsapp and email, activation either sends an opening message to every contact or only arms the agent to answer people who write in, depending on how the campaign is configured. Campaigns start as drafts, so nothing reaches a customer until you activate one.

The Quickstart creates a first campaign end to end. This page is the full reference: the object, the state machine, how contacts are worked through, and the limits that apply.

How it works

POST /api/campaigns PATCH .../activate PATCH .../pause paused by Jelliu (blocked_reason set) PATCH .../activate no pending contacts and no calls in flight draft active paused completed
  1. Create the campaign. It is stored in draft and nothing is sent.
  2. Add contacts to it, one at a time, in bulk or from a CSV file. See Contacts.
  3. Activate it. Jelliu runs the channel’s preflight checks, flips the status to active and queues the outreach.
  4. Workers process each contact. Before every dial or send they re-read the campaign, so a pause or a channel change takes effect on jobs that are already queued.
  5. Completion is automatic. A campaign moves to completed when it has no pending contacts and no call still in progress. The check runs whenever a contact settles, and a background sweep asks again every 15 minutes.

Status transitions

Only two endpoints change the status: activate and pause. There is no endpoint that completes or archives a campaign.

Current statusRequestResult
draftPATCH /api/campaigns/{campaignId}/activateactive, and outreach is queued.
pausedPATCH /api/campaigns/{campaignId}/activateactive. Only contacts still pending are queued again, so the run resumes where it stopped.
activePATCH /api/campaigns/{campaignId}/activate200 with the campaign unchanged. Nothing is queued a second time.
completed, archivedPATCH /api/campaigns/{campaignId}/activate400 CAMPAIGN_NOT_ACTIVE: Campaign can only be activated from DRAFT or PAUSED status
activePATCH /api/campaigns/{campaignId}/pausepaused.
any otherPATCH /api/campaigns/{campaignId}/pause400 CAMPAIGN_NOT_ACTIVE: Campaign can only be paused when ACTIVE
activenone (automatic)completed once no contact is pending and no call is in flight.
activenone (automatic)paused with blocked_reason set, when the problem belongs to the workspace rather than to a contact. See Paused by Jelliu.

Two other rules depend on the status:

  • PATCH /api/campaigns/{campaignId} is refused for completed and archived campaigns.
  • DELETE /api/campaigns/{campaignId} is refused for active campaigns. Pause first.

archived exists in the status enum, but no API operation moves a campaign into it today. Treat it as a terminal status if you encounter it.

Channels

ChannelWhat activation does
voiceQueues one dial per pending contact. Calls are placed within the schedule, paced by the concurrency limits, and retried when nobody answers.
whatsappWith whatsappTemplateId set: sends that approved template to every contact. Without it: the campaign is inbound-only and the agent answers people who write to your WhatsApp number.
emailWith both emailSubject and emailBody set: sends the email to every contact. With neither: inbound-only. Setting just one of them is refused at activation.
webchatInbound-only. Activation arms the agent; nothing is sent.

The agent must serve the campaign’s channel (its channels list). A mismatch is refused on create, on a channel change and again on activation, with 400 VALIDATION_FAILED.

The schedule applies to every channel that sends: voice dials, WhatsApp template sends and email sends. Outside the window, a WhatsApp or email send waits for the next opening in the campaign’s timezone, and the contact stays pending meanwhile. A schedule with no upcoming window, or one that cannot be read, marks the contact failed instead of sending. A contact postponed more than 30 times is also marked failed. Inbound-only campaigns send nothing, so the schedule does not limit when the agent answers.

How voice contacts are worked through

For every queued contact, the dialer checks, in order:

  1. The campaign is still active and still a voice campaign. Otherwise the job ends and the contact stays pending.
  2. The schedule. Outside the window, the dial is postponed to the start of the next enabled window, up to 14 times. A contact that exceeds that, or a schedule with no upcoming window at all, is marked failed.
  3. The contact is still pending, and its current phone number is used, not the one it had when the campaign was activated.
  4. The number is dialable. A value that is not E.164, or that the carrier lookup reports as unroutable, marks the contact invalid without spending an attempt.
  5. Consent. If the workspace requires consent before calling, a contact without voice-call consent is marked invalid.
  6. Claim. The contact is atomically moved to called, so two workers can never dial the same person.
  7. The call. When compliance rules refuse the call, the contact’s status depends on the rule: outside allowed calling hours or over the daily attempt limit, the dial is rescheduled for when calling is allowed again and the contact returns to pending; over the total attempt limit it is marked failed; a blocked prefix or a destination that is not allowed marks it invalid; a contact with no prior opt-in on record, when your workspace requires one, is also marked invalid and the campaign carries on; only the opt-out list or do-not-call marks it dnc. See Compliance checks and Calls for everything that happens once the call is placed.

Concurrency. A call is placed only if the campaign has fewer live calls than its max_concurrent_calls and the workspace has a free slot under its plan’s concurrent-call limit. When either limit is reached, the dial is not failed: the contact returns to pending and the dial is rescheduled with a backoff of 30 seconds growing to 10 minutes.

Retries. When a call ends without reaching the person (no answer, busy line, or a call that never connected), Jelliu schedules another dial after retry_interval_minutes. max_retry_attempts is the total number of dials per contact in a run, including the first one: with the default of 3, a contact is called at most three times. 0 disables retries. Retries go back through the same checks, so they also respect the schedule. Contacts that are converted, dnc or invalid are never retried.

Campaign context reaches the agent

name, productContext and targetAudience are not just labels. On every campaign call they are passed to the agent as the campaign_name, product_context and target_audience variables, together with the contact’s name and your company name. Text conversations (WhatsApp, email, web chat) with a contact that belongs to a campaign receive the same three variables. Write productContext as the brief you would give a human agent: what is being offered and what the conversation is for.

Paused by Jelliu

Some failures belong to the workspace, not to a contact. When a dial fails because the workspace has no phone number of its own (PHONE_NUMBER_REQUIRED) or because the campaign’s agent is paused (AGENT_PAUSED), Jelliu pauses the campaign, writes the reason to blocked_reason and leaves every remaining contact pending. Fix the cause and activate the campaign again to resume.

blocked_reason is not cleared when the campaign is reactivated. Read it together with status: it only describes the current state while the campaign is paused.

The campaign object

Campaign responses are the stored row, with snake_case field names, even though request bodies use camelCase.

retry_policy, voicemail_action and voicemail_message were removed from campaign responses on 2026-09-15. They were legacy columns nothing ever read and nothing could set through this API. Redials come from maxRetryAttempts and retryIntervalMinutes; voicemail detection is an agent setting (enableVoicemailDetection, see Agents). retry_policy on POST /api/onboarding/templates is now refused with 400 VALIDATION_FAILED as well.

FieldTypeNullableDescription
idstring (uuid)NoUnique identifier.
tenant_idstring (uuid)NoThe workspace that owns the campaign.
agent_idstring (uuid)NoThe agent that runs the campaign. Cannot be changed after creation.
namestringNo1 to 200 characters. Reaches the agent as campaign_name.
product_contextstringNo10 to 5000 characters. Reaches the agent as product_context.
target_audiencestringNo1 to 1000 characters. Reaches the agent as target_audience.
statusstringNodraft, active, paused, completed or archived.
blocked_reasonstringYesWhy Jelliu paused the campaign, in Spanish, naming what to fix. null when a person paused it or it was never paused by the system.
channelstringNovoice, whatsapp, email or webchat.
categorystringNosales, support, scheduling, surveys, collections, retention, notifications, interview, language_assessment or general. Decides which call outcomes count as a success.
scheduleobjectNo{ timezone, days }. See Schedule.
max_concurrent_callsintegerNoLive calls this campaign may hold at once. Stored already clamped to your plan’s concurrent-call limit.
max_retry_attemptsintegerNoTotal dials per contact in a run, including the first. 0 disables retries.
retry_interval_minutesintegerNoMinutes between a missed call and the next dial.
whatsapp_template_idstring (uuid)YesWhatsApp only. The approved template sent on activation. null means inbound-only.
whatsapp_template_variablesarray or objectYesWhatsApp only. Campaign-wide values for the template placeholders.
whatsapp_template_variable_mapobjectYesWhatsApp only. Placeholders filled per contact. See Template variables.
email_subjectstringYesEmail only. Subject line, may contain tokens.
email_bodystringYesEmail only. Message body, may contain tokens.
created_atstring (date-time)NoWhen the campaign was created.
updated_atstring (date-time)NoLast change to the row.
deleted_atstring (date-time)YesAlways null in responses: deleted campaigns are not returned.

Fields added by the list endpoint

GET /api/campaigns returns a lighter row. It includes id, tenant_id, agent_id, name, status, category, channel, max_concurrent_calls, max_retry_attempts, schedule, blocked_reason, created_at, updated_at and deleted_at, plus these aggregates. It does not include product_context, target_audience, retry_interval_minutes or the WhatsApp and email fields; fetch the campaign by ID for those.

FieldTypeNullableDescription
agent_namestringYesName of the campaign’s agent.
total_contactsintegerNoContacts in the campaign.
pending_contactsintegerNoContacts with status pending.
called_contactsintegerNoContacts that have been dialed at least once.
converted_contactsintegerNoContacts with status converted.
conversion_ratenumberNoconverted_contacts divided by called_contacts, as a percentage with one decimal. 0 when nobody has been called.

called_contacts and conversion_rate count phone calls only. On whatsapp and email campaigns they stay at 0 even after every contact has been messaged. Use total_contacts and pending_contacts to track progress on those channels.

Schedule

FieldTypeRules
timezonestringA valid IANA time zone, for example America/Bogota.
daysarrayAt least one item, and at least one item with enabled: true.
days[].daystringmonday through sunday.
days[].startHourinteger0 to 23. The first hour calls may start, in timezone.
days[].endHourinteger1 to 24. Calls start only before this hour.
days[].enabledbooleanDays with false are skipped. On enabled days startHour must be lower than endHour.

Hours are whole hours: startHour: 9, endHour: 18 allows calls and sends to start from 09:00 up to 17:59. A day that does not appear in days is treated as disabled. The same window applies to voice, WhatsApp and email campaigns.

Template variables

WhatsApp templates contain numbered placeholders such as {{1}}. You can fill them two ways, and combine them:

  • whatsappTemplateVariables: the same value for every contact. Either an array of up to 20 strings, or an object keyed by placeholder number (keys up to 8 characters). Values are up to 500 characters.
  • whatsappTemplateVariableMap: a value resolved per contact, keyed by placeholder number (keys up to 3 characters). Each entry is either a token string, or { "token": "...", "fallback": "..." } with a fallback of up to 500 characters. Placeholders the map does not mention keep their campaign-wide value.

Email campaigns write the same tokens inline in emailSubject and emailBody, wrapped in double braces, for example Hola {{contact.first_name}}.

TokenResolves to
contact.nameThe contact’s name.
contact.first_nameThe first word of the contact’s name.
contact.emailThe contact’s email.
contact.phoneThe contact’s WhatsApp number, or phone number, when it is a real E.164 number.
campaign.nameThe campaign’s name.
contact.metadata.KEYA custom field of the contact, where KEY is 1 to 64 letters, digits, spaces, _, . or -. CSV imports store column headers here.

Unknown tokens are rejected when you save the campaign. A mapped placeholder number that the template does not have is rejected too.

A contact whose tokens do not resolve, and whose map entry has no fallback, is skipped rather than sent a message with a gap in it. Skipped contacts, and contacts with no WhatsApp number or email address, are set to invalid during activation. Adding the missing data later does not put them back in the queue.

Common tasks

Launch a voice campaign

1

Create the campaign

Creating campaigns requires a full key.

curl -sS -X POST "https://api.jelliu.co/api/campaigns" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"name": "Renovaciones septiembre",
"productContext": "Llamadas a clientes cuyo plan de internet hogar vence este mes para ofrecer la renovación con 20% de descuento.",
"targetAudience": "Clientes residenciales con contrato por vencer",
"channel": "voice",
"category": "retention",
"maxConcurrentCalls": 5,
"maxRetryAttempts": 3,
"retryIntervalMinutes": 120,
"schedule": {
"timezone": "America/Bogota",
"days": [
{ "day": "monday", "startHour": 9, "endHour": 18, "enabled": true },
{ "day": "tuesday", "startHour": 9, "endHour": 18, "enabled": true },
{ "day": "wednesday", "startHour": 9, "endHour": 18, "enabled": true },
{ "day": "thursday", "startHour": 9, "endHour": 18, "enabled": true },
{ "day": "friday", "startHour": 9, "endHour": 17, "enabled": true },
{ "day": "saturday", "startHour": 9, "endHour": 13, "enabled": false }
]
}
}'

The response is 201 Created:

{
"data": {
"id": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90",
"tenant_id": "3e9a1c47-2b8d-4f60-a5c1-7d2e9b4f8a13",
"agent_id": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"blocked_reason": null,
"name": "Renovaciones septiembre",
"product_context": "Llamadas a clientes cuyo plan de internet hogar vence este mes para ofrecer la renovación con 20% de descuento.",
"target_audience": "Clientes residenciales con contrato por vencer",
"status": "draft",
"schedule": {
"timezone": "America/Bogota",
"days": [
{ "day": "monday", "startHour": 9, "endHour": 18, "enabled": true },
{ "day": "tuesday", "startHour": 9, "endHour": 18, "enabled": true },
{ "day": "wednesday", "startHour": 9, "endHour": 18, "enabled": true },
{ "day": "thursday", "startHour": 9, "endHour": 18, "enabled": true },
{ "day": "friday", "startHour": 9, "endHour": 17, "enabled": true },
{ "day": "saturday", "startHour": 9, "endHour": 13, "enabled": false }
]
},
"max_concurrent_calls": 3,
"max_retry_attempts": 3,
"retry_interval_minutes": 120,
"channel": "voice",
"category": "retention",
"whatsapp_template_id": null,
"whatsapp_template_variables": null,
"whatsapp_template_variable_map": null,
"email_subject": null,
"email_body": null,
"created_at": "2026-09-15T14:02:11.482Z",
"updated_at": "2026-09-15T14:02:11.482Z",
"deleted_at": null
}
}

In this example the workspace is on Starter, so the requested maxConcurrentCalls of 5 was stored as 3, the plan’s concurrent-call limit.

2

Add contacts

Load the people to call. For more than a handful, use one bulk request (up to 5,000 contacts) or a CSV upload, because bulk and CSV imports are limited to 5 requests per minute. See Contacts.

curl -sS -X POST "https://api.jelliu.co/api/campaigns/0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90/contacts" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phoneNumber": "+573001234567", "name": "Ana Gómez" }'
3

Activate

curl -sS -X PATCH "https://api.jelliu.co/api/campaigns/0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90/activate" \
-H "Authorization: Bearer $JELLIU_API_KEY"

The response is 200 with the campaign in data, now "status": "active". Activation checks, in this order:

  1. The campaign is draft or paused.
  2. The agent still exists and serves the campaign’s channel.
  3. Voice: at least one contact is pending. Other channels: the campaign has at least one contact.
  4. WhatsApp with a template: the template exists, is approved, every placeholder has a value or a mapping, and your WhatsApp sender is online.
  5. Email: the workspace sends from its own connected mailbox, the mailbox is healthy, and subject and body are either both set or both empty.
  6. Voice: the number of pending contacts does not exceed your plan’s contact limit.
  7. Your plan still has a free active-campaign slot.

Activation starts real calls and messages. The request returns once the outreach is queued, which can take a few seconds for large campaigns.

Reach out on WhatsApp with per-contact values

This campaign sends an approved template whose body is Hola {{1}}, tu pedido de {{2}} está listo. Placeholder 1 is the contact’s first name, falling back to cliente. Placeholder 2 is the same for everybody.

curl -sS -X POST "https://api.jelliu.co/api/campaigns" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agentId": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"name": "Pedidos listos para recoger",
"productContext": "Avisar a clientes que su pedido está listo y resolver dudas sobre horarios de recogida.",
"targetAudience": "Clientes con pedidos listos en tienda",
"channel": "whatsapp",
"category": "notifications",
"whatsappTemplateId": "9b2e4d61-8f3a-4c7b-b1e5-2a6d9c0f4e87",
"whatsappTemplateVariables": { "2": "Tienda Chapinero" },
"whatsappTemplateVariableMap": {
"1": { "token": "contact.first_name", "fallback": "cliente" }
},
"schedule": {
"timezone": "America/Bogota",
"days": [{ "day": "monday", "startHour": 8, "endHour": 20, "enabled": true }]
}
}'

On activation, contacts that have a valid E.164 phoneNumber but no WhatsApp number get their phone number copied into the WhatsApp field, so imported lists are reachable. Template sends only go out inside the campaign’s schedule.

To turn a WhatsApp campaign back into an inbound-only one, send "whatsappTemplateId": null in a PATCH. For email, send "emailSubject": null and "emailBody": null.

Turn an email campaign into outreach

PATCH accepts every create field except agentId, all optional. This request adds a subject and body to an existing email campaign, so its next activation mails every contact.

curl -sS -X PATCH "https://api.jelliu.co/api/campaigns/0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90" \
-H "Authorization: Bearer $JELLIU_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emailSubject": "{{contact.first_name}}, tu renovación está lista",
"emailBody": "Hola {{contact.first_name}}, responde a este correo y te ayudamos a renovar tu plan hoy mismo."
}'

The response is 200 with the updated campaign. A PATCH with no recognized fields returns the campaign unchanged.

An email campaign needs a mailbox of your own. Connect Gmail, Outlook or Zoho Mail under Integrations, then select it as the sender under Settings → Account → Email. Connecting it is not enough. Without that, creating or activating an email campaign fails with 409 VALIDATION_FAILED; mail is never sent from a platform address instead.

Changing channel on an active campaign stops the old channel’s queued jobs but does not queue anything on the new one. Pause the campaign and activate it again so the remaining pending contacts are queued on the new channel.

Pause and resume

Pausing does not delete queued work. Each queued dial or send checks the status when its turn comes and does nothing while the campaign is paused. Calls already in progress finish normally.

# Pause
curl -sS -X PATCH "https://api.jelliu.co/api/campaigns/0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90/pause" \
-H "Authorization: Bearer $JELLIU_API_KEY"
# Resume: activate again
curl -sS -X PATCH "https://api.jelliu.co/api/campaigns/0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90/activate" \
-H "Authorization: Bearer $JELLIU_API_KEY"

A resumed voice campaign queues only the contacts still pending. People already called in the earlier run are not called again, except through the normal retry rules.

List campaigns and track progress

GET /api/campaigns uses page-number pagination: page (default 1) and limit (1 to 100, default 20), newest first.

curl -sS "https://api.jelliu.co/api/campaigns?page=1&limit=20" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": [
{
"id": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90",
"tenant_id": "3e9a1c47-2b8d-4f60-a5c1-7d2e9b4f8a13",
"agent_id": "7c1d9f30-5e2a-4a7e-9c8f-91a0b1c2d3e4",
"name": "Renovaciones septiembre",
"status": "active",
"category": "retention",
"channel": "voice",
"max_concurrent_calls": 3,
"max_retry_attempts": 3,
"schedule": {
"timezone": "America/Bogota",
"days": [{ "day": "monday", "startHour": 9, "endHour": 18, "enabled": true }]
},
"created_at": "2026-09-15T14:02:11.482Z",
"updated_at": "2026-09-15T14:05:40.019Z",
"deleted_at": null,
"blocked_reason": null,
"agent_name": "Laura - Renovaciones",
"total_contacts": 250,
"pending_contacts": 164,
"called_contacts": 86,
"converted_contacts": 12,
"conversion_rate": 14
}
],
"meta": { "page": 1, "limit": 20 }
}

The response has no total count: stop when a page returns fewer than limit items. GET /api/campaigns/{campaignId} returns one campaign with every field from the campaign object, in data.

Campaign reads are cached briefly: the list for up to 60 seconds and a single campaign for up to 120 seconds. Your own create, update, activate, pause and delete requests refresh the cache immediately, but changes Jelliu makes on its own (automatic completion, a system pause, contacts being worked through) can take that long to appear. Use the campaign.completed webhook rather than polling for completion.

Read campaign results

GET /api/analytics/campaigns/{campaignId} summarizes the campaign’s calls. The optional direction query parameter accepts inbound, outbound or all (default).

curl -sS "https://api.jelliu.co/api/analytics/campaigns/0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90" \
-H "Authorization: Bearer $JELLIU_API_KEY"
{
"data": {
"campaignId": "0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90",
"channel": "voice",
"category": "retention",
"totalInteractions": 131,
"completedInteractions": 86,
"effectiveCompleted": 61,
"noEffectiveContact": 25,
"analysed": 74,
"successes": 12,
"successRate": 19.67,
"successRateLabel": "retentionRate",
"averageDurationSeconds": 143,
"avgSentiment": 0.21,
"outcomeBreakdown": {
"customer_retained": 11,
"customer_reactivated": 1,
"callback_scheduled": 9,
"rejected": 18,
"no_answer": 27,
"no_effective_contact": 20
},
"excludedMetrics": []
}
}
FieldDescription
totalInteractionsCalls in the campaign, any status.
completedInteractionsCalls with status completed.
effectiveCompletedOf those, the ones that lasted long enough and carried enough turns to be a conversation. The denominator of successRate.
noEffectiveContactThe rest of completedInteractions: connected calls that never became conversations. Neither a success nor a refusal.
analysedCalls an evaluation actually ran on. analysed of totalInteractions is the rate’s coverage.
successesCalls whose outcome counts as a success for the campaign’s category. For retention, that is customer_retained and customer_reactivated; for sales, sale_closed.
successRatesuccesses divided by effectiveCompleted, as a percentage from 0 to 100 with two decimals. Until 2026-09-16 it divided by completedInteractions, which dragged the rate down with calls nobody could have converted.
successRateLabelWhat the rate means for the category, for example conversionRate, resolutionRate, bookingRate or retentionRate.
averageDurationSecondsAverage length of completed calls. null for non-voice campaigns.
avgSentimentAverage sentiment from -1 to 1 over calls that were scored. null when none were, never 0 for “no data”.
outcomeBreakdownCount of calls per outcome, plus a no_effective_contact key for calls reclassified out of the outcome analysis stamped on them. It is a reporting bucket, not a stored outcome. See Calls for the outcome values and Counting and coverage for the bucket.
excludedMetricsMetrics that do not apply to this channel or category.
_degradedPresent only when part of the computation failed; lists which parts. Treat the related fields as incomplete.

Results are cached for up to two minutes. For WhatsApp and email campaigns this endpoint reports zeros, because they produce conversations rather than calls; read those through Conversations. For call-by-call detail, list GET /api/calls?campaignId=....

Delete a campaign

Deletion is a soft delete: the campaign stops appearing in the API and its ID returns 404. An active campaign must be paused first.

curl -sS -X DELETE "https://api.jelliu.co/api/campaigns/0f3c8b52-6d1a-4e2f-9b7c-4a5e6d7f8a90" \
-H "Authorization: Bearer $JELLIU_API_KEY"

A successful delete returns 204 No Content with an empty body.

Errors

Every error uses the standard envelope. Codes specific to campaigns:

CodeStatusWhen
VALIDATION_FAILED400The body failed the schema (Invalid campaign input on create, Invalid update input on update, with field errors in details), or the campaign ID is not a UUID (Invalid campaign ID).
VALIDATION_FAILED400The agent does not serve the campaign’s channel.
VALIDATION_FAILED400An unknown token in emailSubject, emailBody or whatsappTemplateVariableMap, or a mapped placeholder the template does not have.
VALIDATION_FAILED400Activation: Campaign has no pending contacts to call (voice), or the campaign has no contacts (other channels).
VALIDATION_FAILED400Activation: the WhatsApp template is missing, not approved, has uncovered placeholders, or the WhatsApp sender is not online.
VALIDATION_FAILED400Activation: only one of emailSubject and emailBody is set, or the sending mailbox reports a problem.
VALIDATION_FAILED400Activation: the voice campaign has more pending contacts than your plan allows.
VALIDATION_FAILED400Cannot update a completed or archived campaign, or Cannot delete an active campaign — pause it first.
VALIDATION_FAILED409An email campaign was created or activated without a selected mailbox of your own, or the mailbox could not be checked.
CAMPAIGN_NOT_ACTIVE400Activating a campaign that is not draft or paused, or pausing one that is not active.
CAMPAIGN_NOT_ACTIVE409Campaign status changed concurrently: another request changed the status first. Read the campaign again.
BILLING_ERROR403Activating would exceed the plan’s active-campaign limit (metadata carries limit, current and tier), or the workspace has no active plan (refused on create too).
FORBIDDEN403A read or write key called a mutating campaign route. These require full.
CAMPAIGN_NOT_FOUND404The campaign does not exist, was deleted, or belongs to another workspace.
AGENT_NOT_FOUND404The agentId on create, or the campaign’s agent on activation or channel change, does not exist in your workspace.
RATE_LIMIT_EXCEEDED429More than 10 campaign mutations in a minute. See Limits.

Limits

Scopes

OperationMinimum scope
GET /api/campaigns, GET /api/campaigns/{campaignId}, GET /api/analytics/campaigns/{campaignId}read
POST /api/campaigns, PATCH /api/campaigns/{campaignId}, DELETE /api/campaigns/{campaignId}full
PATCH /api/campaigns/{campaignId}/activate, PATCH /api/campaigns/{campaignId}/pausefull

Adding contacts to a campaign needs only write. See Authentication.

Rate limits

  • All campaign routes count against the general API limit.
  • Create, update, delete, activate and pause also share the 10 per minute configuration-mutation budget with agent and webhook mutations.
  • Contact imports (/api/campaigns/{campaignId}/contacts/bulk, .../upload-preview and .../upload-confirm) are limited to 5 per minute. Listing contacts, adding one and downloading the CSV template use only the general limit.
  • GET /api/analytics/campaigns/{campaignId} is limited to 30 per minute.

Plan limits

LimitStarterGrowthBusinessEnterprise
Active campaigns13UnlimitedUnlimited
Concurrent calls (workspace)31025No plan limit
Pending contacts per voice activation5002,00020,000999,999
  • Only active campaigns use a slot. Drafts, paused and completed campaigns do not. Pausing a campaign frees its slot; activating one takes it.
  • Drafts do not need a free slot. You can create a campaign while your active campaigns already fill the plan; the limit is checked when you activate it, which is refused with 403 BILLING_ERROR until you pause or finish another campaign. Only a workspace without an active plan is refused on create.
  • maxConcurrentCalls is clamped to your plan’s concurrent-call limit when you save it, so the stored value is the effective one.
  • A workspace with no active plan cannot create or activate campaigns.

Jelliu also creates a system campaign named Manual Conversations to hold contacts from web chat and messages started from the dashboard. It is excluded from GET /api/campaigns and never counts against your active-campaign limit.

Request bounds

FieldBoundsDefault
name1 to 200 charactersRequired
productContext10 to 5000 charactersRequired
targetAudience1 to 1000 charactersRequired
maxConcurrentCalls1 to 500, then clamped to the plan10
maxRetryAttempts0 to 103
retryIntervalMinutes5 to 144060
channelvoice, whatsapp, webchat, emailvoice
categorySee the objectsales
emailSubject1 to 300 charactersNone
emailBody1 to 20000 charactersNone

Webhooks

EventSentWhen
campaign.startedYesThe campaign was activated for the first time. data carries campaign_id, campaign_name, agent_id, channel, previous_status and status.
campaign.resumedYesA paused campaign was activated again. Same fields as campaign.started, with previous_status: "paused". Before 2026-09-27 this was sent as campaign.started.
campaign.pausedYesThe campaign was paused. data.reason is manual (a user paused it), agent_deleted (its agent was deleted) or blocked (Jelliu paused it because the workspace cannot dial; blocked_reason says what to fix).
campaign.completedYesThe campaign moved to completed. data carries campaign_id and campaign_name.
call.completed, call.failedYesA campaign call ended. data.campaign_id identifies the campaign.
contact.created, contact.convertedYesA contact was added to, or converted in, the campaign. data.campaign_id identifies the campaign.
contact.opted_outYesA contact of the campaign opted out, including contacts the campaign settled because their number was already suppressed (basis: "suppression_list"). data.campaign_id identifies the campaign. Called contact.dnc before 2026-09-27.

Narrow deliveries to specific campaigns with the campaign_ids filter. See Webhooks.