Campaigns
A campaign texts one message to an audience: all your contacts, a pipeline stage, a smart list, filter rules or hand-picked contacts. Create it, check the price with quote, then send it, or pass scheduled_at when you create it and we send it at that time. For a series of texts spaced out over days, see Drip campaigns. Test keys can only read campaigns: every POST, PUT, PATCH and DELETE under /campaigns answers 403 test_key_not_allowed, including the ones that only price or count. A campaign id that isn’t a whole number answers 400 (invalid_field, or INVALID_ID on the results and AI routes), and a number that isn’t a campaign on your account answers 404 CAMPAIGN_NOT_FOUND.
#The Campaign object
List campaigns and Get a campaign return these, with live delivery counts. The fields below are the ones most callers need; the object carries every saved setting of the campaign too.
Create, update, pause, resume, unschedule and stop return the campaign as it is saved, which is not quite the same shape: it has total_replies in place of replies, its total_sent is the last saved count rather than the live one, and it has no dlr_delivered, dlr_failed, dlr_pending, delivery_rate, skipped, was_stopped, waiting_note or can_add_recipients. Call Get a campaign after a change to read the full shape.
idinteger- The campaign id.
namestring- Shown in your campaign list.
statusenum- Where the campaign is.
Possible values
draft,scheduled,sending,paused,completed,failedandcancelled. A stopped campaign iscompletedwith fewer sent than its audience. message_bodystring- The text, with merge fields unfilled.
total_recipientsinteger- Audience size.
total_sentinteger- Texts sent so far.
dlr_deliveredstring | null- Carrier delivery count: delivered.
nullwhen we couldn’t read the counts; the answer then hasunavailable: ["delivery_counts"]. dlr_failedstring | null- Carrier delivery count: failed.
nullthe same way asdlr_delivered. dlr_pendingstring | null- Carrier delivery count: pending.
nullthe same way asdlr_delivered. delivery_ratenumber | null- Percent delivered out of delivered plus failed, like
98.2.nulluntil a text resolves. repliesinteger- Replies to this campaign. The same number as
total_replies, which the object also carries. skippedinteger- Recipients the send left out, such as people in quiet hours or opted out.
was_stoppedbooleantruewhen you stopped it, or itsstop_atpassed.waiting_notestring | null- Only on a
scheduledcampaign past itsscheduled_atthat has not started: why it is still waiting (outside its sending hours, or still waiting to start 15 minutes on).nullotherwise. can_add_recipientsbooleantruewhen Add recipients would take more people for it now.scheduled_atISO 8601 string | null- When it’s scheduled to send, or
null. created_atISO 8601 string- When it was created.
#List campaigns
Every campaign on your account, newest first, with live delivery counts. Not paginated. Returns a list of Campaign objects in data. When we couldn’t read the delivery counts, the dlr_* fields are null and the answer has unavailable: ["delivery_counts"].
Parameters
lightboolean · query- Send
light=1for justidandnameof each campaign, in the same order, without delivery counts. Much faster. Any value counts, so leave the parameter out to get the full list. scheduledboolean · query- Send
scheduled=1for only the scheduled campaigns, each withid,name,status,scheduled_at,created_atandtotal_recipients. Any value counts.lightwins when you send both.
#Get a campaign
One campaign, the same Campaign object List campaigns returns for it, with live delivery counts. Use it to follow one campaign while it sends. When we couldn’t read the delivery counts, the dlr_* fields are null and the answer has unavailable: ["delivery_counts"].
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | The id isn’t a whole number. |
| 404 | CAMPAIGN_NOT_FOUND | No campaign with that id on your account, or you deleted it. |
| 500 | CAMPAIGN_LOAD_FAILED | We couldn’t load it. Try again. |
#Create a campaign
Saves a draft, or a scheduled campaign when you pass scheduled_at. Creating doesn’t send; a draft goes out when you call send, and a scheduled campaign is charged when it sends. Needs the send_sms permission. Returns 201 and the campaign as saved (the saved shape), with status draft or scheduled. A create with no name, message or audience hands back your newest blank draft with reused: true instead of making another.
Parameters
namestringRequired- Shown in your campaign list.
More parameters
consent_attestedboolean- Required as
trueto schedule (scheduled_at): your confirmation that every recipient agreed to get texts from you (consent). We store it with the time, your IP and the audience. A draft saves without it; you confirm consent when you schedule or send it. message_bodystring- Required to schedule. Merge fields like
{{contact.first_name}}are filled per contact. Your compliance footer is added the way your settings say. recipient_filterstringDefaultallallfor every active contact, a pipeline stage id for that stage,rulesto userecipient_rules, ormanualto userecipient_contact_ids.segment_idinteger- Send to a smart list.
segment_idis a smart list id, not a text segment. recipient_rulesobject[]- Filter rules, the same shape a smart list uses. Use with
recipient_filter: "rules". Rules that match nobody send to nobody, never everyone. recipient_contact_idsinteger[]- Hand-picked contact ids, up to 100,000.
scheduled_atstring- When to send. With
Zor an offset it’s that exact time; without one it’s read intimezone, else your account’s time zone. Must be at least 1 minute from now, or it is refused with a400. timezonestring- IANA name such as
America/Chicago. phone_number_idinteger- Send from this number. Leave it out to use your default.
phone_number_idsinteger[]- Send from two or more of your active numbers, taking turns as
from_strategysays. Ignored when you passphone_number_id. One number here is not kept; usephone_number_idfor one. from_strategyenumDefaultlocal- How we pick among your numbers when
phone_number_idisn’t set.localpicks the number that has sent the least among those in the recipient’s area code when local presence is on, and your default number when it is off.even(take turns),randomanddeliverability(the number that has sent the least) spread across all your numbers either way; with local presence on,deliverabilitypicks the same number aslocal. Any other value is read aslocal. smart_send_enabledbooleantruetexts each contact at the hour they usually reply, inside your delivery window, so the send goes out over time. Price it with quote and the same flag.quiet_hours_modeenumDefaultdefer- A recipient in their quiet hours:
deferholds the text and sends it when their quiet hours end, if that is within 24 hours of the send starting and before any deadline in the message; otherwise they are skipped.skipleaves them out. media_urlstring- An http(s) link to an image or PDF. Sends as MMS.
delivery_window_startstring- Send only between this time and
delivery_window_end, like09:00. delivery_window_endstring- The end of that window, like
17:00. delivery_window_daysstring- Days to send on, as lowercase day names separated by commas, like
mon,tue,wed. Usesun,mon,tue,wed,thu,friorsat. send_rate_per_secinteger- Texts per second. Capped at your account’s limit (
GET /campaigns/send-cap). drip_interval_secinteger- Seconds between each text, for a slow send. 7 is about 500 an hour.
stop_atstring- Stop sending at this time. A follow-up that comes due after it is not sent.
stop_after_messagesinteger- Stop after this many sent: a whole number above 0. Leave it out, or send
nullor0, for no limit; any other value is refused withstop_rule_invalid. stop_after_failuresinteger- Stop after this many failed: a whole number above 0. Leave it out, or send
nullor0, for no limit; any other value is refused withstop_rule_invalid. stop_combinatorenumDefaultany- Ignored: the first stop rule reached (
stop_at,stop_after_messagesorstop_after_failures) ends the send. is_ab_testbooleantruemakes it an A/B test:message_bodyis version A,message_variantsholds the others.message_variantsstring[]- The other versions of the text in an A/B test. Ignored unless
is_ab_testistrue. ab_splitintegerDefault50- The percent that gets A when there are two versions.
tag_on_sendstring- Tag every contact who gets the text.
reply_keywordstring- A word people reply with, like
YES. We keep letters and digits only, upper case, up to 20. An automation that runs on replies to a campaign matches a reply containing it to this campaign, even if the contact got a newer one since. followup_enabledbooleantruesends one follow-up text to people who didn’t reply afterfollowup_delay_hours. It is not sent afterstop_ator a Stop, and it skips anyone whose merge field in the follow-up is blank. The follow-up is charged like any campaign text, for each person it goes to, from your balance. If the campaign has a picture, the follow-up sends it again and is charged as a picture message. Needsfollowup_message: without one the follow-up is saved off. Kept for existing integrations: for a new follow-up, add a step to a drip campaign, which is how the dashboard builds one.followup_messagestring- The follow-up text. Required when
followup_enabledistrue. followup_delay_hoursinteger- Hours without a reply before the follow-up goes out.
With scheduled_at, a create can answer 200 with success: false and a needs_confirmation field (empty_merge_field or duplicate_campaign) instead of saving. Nothing was saved. Read message, then repeat the same request with one of the answers Send takes: confirm_empty_merge, skip_empty_merge, confirm_duplicate or duplicate_exclude_blast_id.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | campaign_name_required | No name. |
| 400 | consent_required | You passed scheduled_at without consent_attested: true. |
| 400 | message_required | No message on a scheduled or send-now campaign. |
| 400 | message_length_invalid | The message is over 25 segments, or over 1,600 characters with media_url. |
| 400 | media_url_invalid | media_url isn’t an http(s) link. |
| 400 | variants_invalid | message_variants isn’t a list of texts. |
| 400 | ab_split_invalid | ab_split isn’t a whole number from 1 to 99. |
| 400 | timezone_unknown | timezone isn’t an IANA name we know, like America/Chicago. |
| 400 | delivery_window_invalid | The delivery window ends before it starts. |
| 400 | invalid_field | A field has the wrong type or format, such as a window time that isn’t 24-hour HH:MM or a quiet_hours_mode that isn’t defer or skip. error says which. |
| 400 | AUDIENCE_TOO_LARGE | With scheduled_at, the audience is over 500,000 recipients, the most one campaign sends to. Nothing was saved. Narrow it with a smart list or filter, or split it into smaller campaigns. |
| 400 | schedule_too_soon | scheduled_at is in the past or too close to now (under the minimum lead time). |
| 400 | stop_rule_invalid | stop_at isn’t a date and time, is less than a minute away, or is more than a year away, or stop_after_messages / stop_after_failures is not a whole number above 0. |
| 400 | pace_rate_invalid | send_rate_per_sec isn’t a whole number of at least 1. |
| 400 | pace_sequence_invalid | drip_interval_sec isn’t a whole number of at least 2. |
| 400 | pace_field_unknown | The body has interval_sec, per_second, per_minute, per_hour or per_day. Those are for PATCH /campaigns/:id/pace; here, send drip_interval_sec or send_rate_per_sec. |
| 400 | TOO_MANY_MANUAL_RECIPIENTS | More than 100,000 hand-picked contacts. |
| 400 | NOT_ASSIGNED_TO_YOU | Your seat only reaches its assigned contacts, and every hand-picked contact belongs to someone else. |
| 400 | SEGMENT_NOT_FOUND | The smart list doesn’t exist. |
| 400 | SEGMENT_RULES_INVALID | The smart list’s saved rules are damaged. |
| 400 | NO_RECIPIENTS | Nobody in the audience can be texted. Only for a scheduled campaign; a draft can be empty. |
| 400 | no_recipients | You answered duplicate_exclude_blast_id and everyone was in that other campaign. |
| 400 | number_not_active | phone_number_id isn’t an active number on your account. |
| 400 | sender_number_inactive | A number in phone_number_ids isn’t an active number on your account. |
| 400 | CONTENT_BLOCKED | The message breaks our content rules. Nothing was saved. error names the word that matched when it can, and matched lists what matched. message is a fixed sentence. |
| 402 | BILLING_PAST_DUE | Your last payment failed. Update your card in Billing. |
| 403 | number_not_assigned | Your seat sends only from its assigned numbers, and you picked another one. |
| 409 | assigned_number_required | With scheduled_at: your seat sends only from its assigned numbers, so pick one with phone_number_id. |
| 500 | CAMPAIGN_CREATE_UNCONFIRMED | Something broke while we saved it. List your campaigns before you try again. |
| 503 | CONTENT_REVIEW_UNAVAILABLE | We couldn’t finish checking the message, so nothing was saved. Try again shortly. |
#Update a campaign
Changes a draft, scheduled or paused campaign. Send only the fields you want to change; they take the same values as create. Needs the send_sms permission. Returns the campaign as saved (the saved shape). Like create, a save that schedules can answer 200 with needs_confirmation instead; nothing was saved, so repeat it with your answer.
Parameters
consent_attestedboolean- Required as
truewhen the save needs a new consent record: you passscheduled_atand the campaign has none yet, or the campaign ends up scheduled (or is paused) and its audience changed, in this save or in an earlier save as a draft.
Errors
Besides these, an update answers the same 400 field errors as create.
| HTTP | Code | When |
|---|---|---|
| 400 | NOT_EDITABLE | It has started sending or finished. Duplicate it to send a new version. |
| 400 | PARTIALLY_SENT_LOCKED | It already went out to part of its audience, so its message, attachment and recipients can’t change. |
| 400 | CHARGED_AUDIENCE_LOCKED | It was already charged for its recipients, so its message and recipients can’t change. |
| 400 | CHARGED_MEDIA_LOCKED | It was already charged, so its attachment can’t change. |
| 400 | AUDIENCE_TOO_LARGE | The save schedules the campaign, or changes a scheduled one’s audience, and the audience is over 500,000 recipients. Nothing was saved. |
| 403 | attestation_required | The save needs a new consent record (see consent_attested) and you didn’t pass consent_attested: true. Nothing was saved. |
| 404 | CAMPAIGN_NOT_FOUND | No campaign with that id on your account. |
| 409 | state_changed | Its status changed while we saved. Nothing was saved. |
| 409 | ALREADY_SENT | It already texted some people, so it can’t go back to a draft. Nothing was saved. |
#Delete a campaign
A draft, scheduled or failed campaign that never texted anyone is deleted for good. Any other finished campaign (completed, cancelled, or failed after texting people) is taken off your list instead: List campaigns and Get a campaign stop showing it, and its texts stay in your history. If an unsent campaign still holds money we charged for it, we put that back on your balance before deleting it. Stop a sending or paused campaign first. Needs the send_sms permission. Returns {"success": true}.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | The id isn’t a whole number. |
| 404 | CAMPAIGN_NOT_FOUND | No campaign with that id on your account, or it’s already deleted. |
| 409 | not_deletable | It’s sending or paused. Stop it, then delete it. status says which. |
| 409 | refund_pending | We couldn’t put its held charge back yet, so it wasn’t deleted. It won’t send. Try again in a minute. |
| 500 | DELETE_FAILED | The delete didn’t go through. Check the campaign, then try again. |
#Duplicate a campaign
Copies a campaign into a new draft named Copy of plus its name. The copy keeps the message, audience, numbers, pace, stop rules and other settings; its counts, schedule and consent record start fresh, so you confirm consent again when you send it. Free; nothing is sent. Send an Idempotency-Key header to retry without making a second copy (Idempotency). Needs the send_sms permission. Returns the new draft as saved (the saved shape) in data.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | The id isn’t a whole number. |
| 404 | CAMPAIGN_NOT_FOUND | No campaign with that id on your account. |
| 500 | CAMPAIGN_LOAD_FAILED | We couldn’t load it, so no copy was made. |
| 500 | DUPLICATE_FAILED | No copy was made. Try again. |
#Price a campaign
What a send would cost, priced the way we charge it: every recipient’s text with merge fields filled and your footer on. A follow-up (followup_enabled) is not in this price; it is charged when it goes out. Nothing is saved or sent. Needs the send_sms permission.
Parameters
Takes the same audience parameters as Create a campaign: recipient_filter, segment_id, recipient_rules and recipient_contact_ids.
messagesobject[]Required- 1 to 20 messages, each
{"body": "..."}, or{"media": true}for MMS. A message can also carryvariants(extra versions of the text) andab_split(the share that gets the first version), which change the price the same way they change the send. scheduledboolean- Price it as a scheduled send. A scheduled send can add the compliance surcharge, a per-segment charge for number validation and TCPA scrubbing, each when it’s on (see Billing; your per-segment price is
compliance_usd_per_segmentin Get your balance). smart_send_enabledboolean- Price it with smart send on.
drip_interval_secinteger- Seconds between each text, for a slow send.
delivery_window_startstring- Send only inside a daily window that starts at this time. When both ends are set, the send goes through the queue and can add the compliance surcharge.
delivery_window_endstring- The end of that daily window.
Returns
data.recipientsinteger- How many people it would go to.
data.messages[]object[]media,segmentsandcharge_centsfor each message.segmentsis the total over every recipient, ornullfor a picture message, which is priced per recipient.data.charge_centsinteger- The message charges, in cents, before the compliance surcharge. When
screening_per_unit_usdis above 0, the price is this plusbillable_unitstimesscreening_per_unit_usd(in dollars). data.billable_unitsinteger- The units it would bill, added up over all messages: segments for a text, one per recipient for a picture message.
data.screening_per_unit_usdnumber- The compliance surcharge per segment when this send owes it (number validation or TCPA scrubbing on, and the send goes out over time), else 0.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | QUOTE_BAD_MESSAGES | No messages, more than 20, or a text (or one of its variants) over 25 segments. error says which. |
| 500 | QUOTE_UNAVAILABLE | We couldn’t price it right now. Try again. |
#Preview an audience
Who an audience would reach before you create or send a campaign: the active, textable contacts it matches, by the same rules the send uses. Nothing is saved or sent, and it’s free. Needs the send_sms permission.
Parameters
Takes the same audience parameters as Create a campaign: recipient_filter, segment_id, recipient_rules and recipient_contact_ids.
count_onlybooleantrueanswers onlytotalandcapped, without phone numbers. The fastest way to count.limitinteger- Only the newest this many matching contacts, up to 50,000.
include_idsbooleantrueadds the matching contact ids asids.include_samplebooleantrueaddssample: up to 200 contacts, each withname,phoneandstage.sample_offsetinteger- With
include_sample, the page of 200 that starts here, 0 to 50,000. The answer addssample_totalandsample_next_offset(nullon the last page).
Returns
data.totalinteger- How many it reaches. Past 50,000 this is a close estimate.
data.phonesstring[]- Their numbers, up to 50,000. Left out with
count_only, on a sample page, and for a teammate when Teammates can export contacts is off: thenexport_restrictedistrueandidsis left out too. data.cappedbooleantruewhen you passedlimit.data.walledboolean- Set when your seat only sees its assigned contacts:
totalcounts everyone the send reaches, and the lists hold only the contacts you can see.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | SEGMENT_NOT_FOUND | The smart list doesn’t exist. |
| 400 | SEGMENT_RULES_INVALID | The smart list’s saved rules are damaged. |
| 400 | invalid_field | count_only isn’t a boolean, or sample_offset is out of range. |
| 500 | RECIPIENT_PREVIEW_FAILED | We couldn’t count the audience. Try again. |
See also: the guide to doing this in the app.