Drip campaigns
A drip campaign sends a series of texts, each a set time after the last. A contact you add gets step 1 after its delay, then step 2, and so on. Anyone who opts out isn’t texted again. A drip can also branch, wait for a reply, tag a contact or call a webhook: create one with any of the step types here or build it in the app, then add contacts from your system. Sending hours, Smart Scheduling, the sending number and the reports are on Drip campaign settings. Test keys can’t call the endpoints that change a drip (403 test_key_not_allowed), and an id that isn’t a drip campaign on your account answers 404 DRIP_NOT_FOUND. Some fields and codes still say sequence, our older name for a drip campaign: sequence_id is a drip campaign id.
A drip has no pause, resume or stop endpoints like a campaign. One call does all three, Set a drip’s status: pause is paused, resume is active and stop is completed (it stops all sending, not a sign every contact finished; set it active again and contacts carry on from their step). It answers a bare {"success": true}, and a missing drip is DRIP_NOT_FOUND where a missing campaign is CAMPAIGN_NOT_FOUND.
#The Drip campaign object
List drip campaigns returns these. Create a drip campaign returns one.
idinteger- The drip campaign id.
namestring- The drip’s name.
statusenumactive,paused,completedordraft. Only anactivedrip sends.steps[]object[]- In order:
step_order,step_type,name,message_body,delay_days,delay_hours,delay_minutes, and the branch and wait fields in Drip steps. Withoutlight, each step also hasleads_at_step: contacts active or waiting on a reply whose next step is this one. total_enrolledinteger- Contacts in it now, finished ones included. Contacts you removed are not counted.
timezonestring or null- The zone its days, sending hours and
start_attimes use. A new drip starts in your account’s time zone.null(your account had no time zone when the drip was made, and you have not set sending hours with a zone) meansAmerica/New_York. delivery_window_start,delivery_window_end,delivery_window_daysstring or null- Its sending hours, from Set a drip’s sending hours.
nullis no limit beyond quiet hours. smart_send_enabledinteger1when Smart Scheduling is on, else0.phone_number_id,from_phone_idsinteger, string or null- The sending number, or the numbers it takes turns between. Both
nullmeans your account picks the number.
Without light, List drip campaigns adds live counts to each drip:
enrolled_countinteger- Contacts still going through it: active or waiting on a reply.
paused_count,completed_count,stopped_countinteger- Contacts paused, finished, or stopped. A low balance pauses contacts of an active drip; they start again on their own, keeping their spacing, once you top up and your balance is at least $1. An opt-out or a removal stops them.
queued_now,past_dueinteger- Active contacts whose next step is due now.
past_duecounts only those with a send time that has passed. queued_today,queued_tomorrow,queued_7dinteger- Active contacts due later today, tomorrow (days in the drip’s time zone), and in the next 7 days.
schedule_14dobject[]- The next 14 days, one
{date, count}per day that has sends. List who is scheduled on a day lists the contacts.
#Drip steps
Every step has a step_type (leave it out for send_sms), an optional name (up to 80 characters) and a wait before it: delay_days, delay_hours, delay_minutes, each from 0 up to 10 years. A day keeps the same clock time in the drip’s time zone, so a 9am text with a 1 day wait goes out at 9am the next day even across a daylight saving change. The first step after you add a contact is the exception: its wait is counted in plain hours from when you added them. message_body holds what the step needs, which depends on its type. A step’s place in the list is its step_order, starting at 1, and branch fields point at a step_order. After any step other than if_else, wait_reply and end, the contact moves to the next step_order; when there is none, the drip is finished for them. The step types and their fields are in Step types at the end of this page.
#List drip campaigns
Every drip campaign on your account, newest first. To pick a drip or read its steps, send light=1: it is fast and returns steps only. Without it, each drip also carries live counts, which can be slow on large accounts. Returns a list of Drip campaign objects in data.
Parameters
idinteger, query- Just this drip, with its counts. The list then holds one drip, or none when it isn’t yours. Ignored with
light. lightboolean, query- Send
light=1for just the drips and their steps, without counts. Much faster on large accounts. Any value counts, so leave the parameter out to get the counts.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | id isn’t a whole number from 1 up. |
| 500 | SEQUENCE_LIST_FAILED | We couldn’t read them. Try again. |
#Create a drip campaign
Creates a drip campaign and its steps. It’s on as soon as it’s saved unless you pass status, but sends nothing until you add contacts. Each text is charged when it sends: its segments times your per-segment rate plus the carrier fee, and the compliance surcharge when your account owes it (Get balance and rates has each price). Needs the send_sms permission. Returns 201 and a Drip campaign object with its steps.
Parameters
namestringRequired- The drip’s name, up to 120 characters. A second create with the same name inside 10 seconds creates nothing: it answers
200(not201) withdeduped: trueand only the first drip’sidandnameindata, so a retry is safe. stepsobject[]- In order, up to 200. Any of the step types, with their branch and wait fields; a step’s place sets its
step_order, so branch fields point at positions counting from 1. Create refuses an unknown type or a loop. It does not check that a branch points at a step that exists; Save a branched drip does. You can leavestepsout and add them later with Replace a drip’s steps. statusenumDefaultactiveactive,pausedordraft.smart_send_enabledboolean- Moves each step after the first toward the hour the contact usually replies, up to 12 hours later.
Returns
dataobject- The drip campaign with its
steps. empty_mergeobject- Merge fields that are blank for some of your contacts, by step. Those contacts skip that step unless it has
allow_empty_merge: true.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | no code | No name. |
| 400 | invalid_field | A name over 120 characters, or more than 200 steps. |
| 400 | DRIP_STATUS_INVALID | status isn’t one of those. |
| 400 | BAD_SHAPE | smart_send_enabled isn’t on or off. |
| 400 | CONTENT_BLOCKED | A step breaks our content rules. step_index says which. The other step refusals are in Drip steps. |
| 402 | BILLING_PAST_DUE | Your last payment failed. Update your card in Billing. FRAUD_HOLD while your account is under a security review. |
| 409 | DRIP_NAME_JUST_USED | A drip with this name but different steps was created seconds ago. |
| 500 | DRIP_CREATE_FAILED | Nothing was saved. Try again. |
| 500 | DRIP_CREATE_PARTIAL | It may have saved. List your drips before you try again. |
#Draft a drip with AI
Turns a plain description into a proposed drip, or into up to 3 questions to answer first. Nothing is saved: pass the steps you keep to Create a drip campaign. Needs the send_sms permission.
Parameters
descriptionstringRequired- What the drip should do, up to 4,000 characters.
force_buildbooleantruealways drafts the steps. Leave it out and the answer may be up to 3 questions to answer first instead of a draft.
Returns
dataobject- Either
questions(each aquestionand itsoptions), or a draft:name,summary,trigger,branchedandstepsin the step shape.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | DESCRIPTION_REQUIRED | No description. |
| 400 | DESCRIPTION_TOO_LONG | Over 4,000 characters. |
| 422 | no code | It produced no steps, or won’t write this drip. error says why. |
| 502 | SEQUENCE_AI_BAD_RESPONSE | The draft came back unreadable. Rephrase and try again. |
| 503 | SEQUENCE_AI_UNAVAILABLE | The AI builder is off. Also a 503 with no code when it has reached its limit for the month. |
#Replace a drip’s steps
Replaces every step of a drip with the ones you send, in order: a step’s place sets its step_order. While contacts are partway through it (active, waiting on a reply or paused), only the wording can change: the same steps, types, waits and branches, with a new name or message_body. Anything else is refused, so nobody is sent down the wrong path. Send each step whole: a field you leave out is cleared, except step_type and media_url, which keep what is stored at that position. Needs the send_sms permission. Returns the saved steps in data, and empty_merge as on create. The steps it replaces are kept as an earlier version: list them with List earlier versions (kind=drip) and put one back with Restore an earlier version.
Parameters
idinteger, pathRequired- The drip campaign id.
stepsobject[]Required- From 1 to 200 steps.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | No steps, or more than 200. |
| 400 | CONTENT_BLOCKED | A step breaks our content rules. The other step refusals are in Drip steps. |
| 409 | DRIP_HAS_ACTIVE_CONTACTS | Contacts are partway through it and this changes more than wording. Nothing was changed. |
| 500 | DRIP_STEPS_SAVE_FAILED | Nothing was saved; the old steps are still in place. Try again. |
#Save a branched drip
Replaces every step of a drip, keeping the step_order you give each one, so your branch fields stay right. Use it for drips with if_else or wait_reply steps. The same rules apply as Replace a drip’s steps, the same errors, and also 400 DRIP_GRAPH_INVALID when a step_order is missing, under 1 or used twice, or a branch points at a step that isn’t in the list. A blank text step is refused wherever it is. Needs the send_sms permission.
Parameters
idinteger, pathRequired- The drip campaign id.
stepsobject[]Required- From 1 to 200 steps, each with its own
step_order. End each branch with anendstep.
#Rename a step
Changes one step’s name and nothing else, even while contacts are partway through. Needs the send_sms permission. Returns {"success": true, "name": "..."} with the name as saved.
Parameters
idinteger, pathRequired- The drip campaign id.
orderinteger, pathRequired- The step’s
step_order. namestring- The new name. Longer than 80 characters is cut to 80.
Errors
| HTTP | Code | When |
|---|---|---|
| 404 | DRIP_STEP_NOT_FOUND | No step at that order. A 400 with this code when order isn’t a number. |
#Add contacts to a drip
Adds contacts to a drip campaign. Each gets step 1 after its delay, inside the drip’s sending window. Each text is charged when it sends: its segments times your per-segment rate plus the carrier fee, and the compliance surcharge when your account owes it (Get balance and rates has each price). A drip that isn’t active holds their texts until you turn it on. Contacts already in it are skipped and counted. Contacts you pick by id who have opted out or can’t be texted are skipped and counted too; a smart list or filter only matches contacts who can be texted, so the others are left out and not counted. If every contact you picked by id is opted out or not textable, nobody is added and you get NOT_TEXTABLE. Needs the send_sms permission.
Parameters
idinteger, pathRequired- The drip campaign id.
More parameters
contact_idsinteger[]- The contacts to add.
consent_attestedboolean- Required when adding more than one contact, unless you send
attestation_id. Must betrue: your confirmation that they all agreed to get texts from you (consent). attestation_idinteger- Instead of
consent_attested: a consent confirmation you already recorded for exactly these phone numbers. Each one is used once. segment_idinteger- Other way to pick the audience: everyone in a smart list.
filter_rulesobject[]- Other way to pick the audience: everyone matching filter rules.
recipient_filterstring- Other way to pick the audience, the shape a campaign takes. It is the same as the fields above:
manualwithrecipient_contact_ids(same ascontact_ids),ruleswithrecipient_rules(same asfilter_rules),allfor every active contact who can be texted, or a pipeline stage id.contact_idswins when any of them are yours. start_atstring- Hold the first text until this time. A date and time like
2027-01-05T09:00is read in the drip’s time zone; a drip with none (made when your account had no time zone, until you set one) usesAmerica/New_York. A time ending inZor an offset like-08:00is exact. A date alone starts at midnight in the drip’s zone. A past time starts now. One we can’t read is refused with400 START_AT_INVALID. spread_daysinteger- Spread first texts evenly over this many days, up to 60.
pace_per_dayinteger- Or send about this many first texts a day: the days are worked out from how many were really added, up to 60. Wins over
spread_days. add_tagstring- Tag every contact this call found, including the ones skipped because they’re already in it.
taggedsays how many. activate_draftbooleantrueturns adraftdrip on before adding contacts. If it can’t, nobody is added (409 DRAFT_NOT_ACTIVATED).
Returns
data.enrolledinteger- Contacts added.
0, with a200, when everyone found is already in it. data.skipped_already_enrolledinteger- Skipped because they’re already in it.
data.skipped_not_textableinteger- Picked by id and skipped because they’re opted out or can’t be texted.
data.skipped_not_foundinteger- Picked by id and skipped because they weren’t found.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | NO_RECIPIENTS | Nobody was found: none of the ids are your contacts, or the list or filter matched nobody who can be texted. |
| 400 | NOT_TEXTABLE | Every contact you picked by id has opted out or can’t be texted. |
| 400 | DRIP_NO_STEPS | The drip has no steps yet. Add one first. |
| 400 | AUDIENCE_TOO_LARGE | A list or filter matched more than 5,000. Narrow it or split it. |
| 400 | RULES_MATCH_NOBODY | The rules can’t match anyone. |
| 400 | FILTER_INVALID | We couldn’t use the filter or rules you sent. |
| 400 | SEGMENT_NOT_FOUND | The smart list doesn’t exist. SEGMENT_HAS_NO_RULES when it has no usable conditions. |
| 400 | START_AT_INVALID | start_at isn’t a date and time we can read. Nobody was enrolled. |
| 400 | RESERVED_TAG | add_tag is the tag we add only on an opt-out. |
| 402 | BILLING_PAST_DUE | Your last payment failed. FRAUD_HOLD while your account is under a security review. |
| 403 | CONSENT_REQUIRED | More than one contact and no consent_attested: true. error_code is attestation_required, as on campaigns. |
| 403 | CONSENT_LIST_INVALID | The attestation_id doesn’t match these contacts, or was already used. |
| 409 | DRAFT_NOT_ACTIVATED | You sent activate_draft and we couldn’t turn the drip on. Nobody was enrolled. |
| 500 | ENROLL_CHECK_FAILED | We couldn’t read the drip or the contacts. Nobody was enrolled; try again. |
| 500 | DRIP_ENROLL_OUTCOME_UNKNOWN | Some may have been added. Check who’s in it before retrying; retrying never adds anyone twice. |
#List contacts in a drip
One page of the contacts in a drip campaign, newest first, with where they are in it. A drip can hold more than one page, so page on with offset until a page comes back shorter than limit.
Parameters
idinteger, pathRequired- The drip campaign id.
limitinteger, queryDefault500- From 1 to 500.
offsetinteger, queryDefault0- How many to skip.
Returns
data[]object[]- One per contact:
contact_id,first_name,last_name,phone,email,status,current_step,enrolled_at,next_send_at, andlast_skip_detailwhen a text was skipped.
#Set a drip’s status
Turns a drip campaign on (active), pauses it (paused), stops it (completed) or moves it to drafts (draft). Only an active drip sends; under any other status contacts keep their place and get nothing. Any status can change to any other, so a drip you stopped can be turned back on and its contacts carry on. A drip that stopped itself because it reached its stop rule has no contacts left in it; add them again after you turn it on. Texts that came due while it was off are spread out when it comes back on, not sent all at once. On a very big drip that can take longer than one request: you get 202 with turning_on: true, the drip stays off and sends nothing until its texts are spread out, and then it turns on by itself. This doesn’t change the automation that adds contacts to the drip. Needs the send_sms permission.
Parameters
idinteger, pathRequired- The drip campaign id.
statusenumRequiredactive,paused,completedordraft.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | status missing or not one of those. |
| 500 | DRIP_STATUS_WRITE_FAILED | Nothing was changed. Try again. |
#Send due texts early
Moves the soonest scheduled sends of an active drip to now, so they go out on the drip’s next pass instead of on their day. It releases at most one day’s pace: the number of contacts due on the next scheduled day after today, less what the drip already sent today and what this call released earlier today (days in the drip’s time zone). The texts go through the same checks as any drip text (sending hours, quiet hours, opt-outs) and are charged like any drip text. Needs the send_sms permission.
Parameters
idinteger, pathRequired- The drip campaign id.
previewbooleantruereturnsready, how many it would release, and moves nothing.
Returns
data.releasedinteger- Sends moved to now. With
paceandsent_today. data.reason_codestring- When nothing was released:
SEQUENCE_PAUSED(the drip isn’t active),NOTHING_SCHEDULEDorDAILY_PACE_REACHED, with a sentence inreason.
Errors
| HTTP | Code | When |
|---|---|---|
| 503 | SEND_NOW_COUNT_UNAVAILABLE | We couldn’t count today’s sends, so nothing was moved. Try again. |
| 500 | SEND_NOW_OUTCOME_UNKNOWN | Some may already be on their way. Check the drip’s sent texts before trying again. Also a 503. |
#Remove contacts from a drip
Removes contacts who haven’t been sent anything from this drip yet, so you can fix a wrong batch and add it again. Anyone already texted stays in: to take one of them out, stop the contact’s drip. Needs the send_sms permission.
Parameters
idinteger, pathRequired- The drip campaign id.
modeenumDefaultallall,after(added afterenrolled_after) orkeep(keep the firstkeep_first).enrolled_afterstring- An ISO 8601 date and time, such as
2026-11-13T14:00:00-05:00. Required formode: "after". Send it withZor an offset: a time without one is read as UTC, not in the drip’s time zone likestart_at. keep_firstinteger- Required for
mode: "keep". A whole number, 0 or more. previewbooleantruereturns thecountwithout removing anyone.after,throughinteger- Only when the last call answered
more: true: send back theafterandthroughit gave you, with the samemode, and the removal carries on where it stopped. Leave both out on the first call.
Returns
removedinteger- How many this call removed.
morebooleantruewhen one call ran out of time on a very big drip. Call again withafterandthroughuntil it’sfalse. Contacts added after the first call aren’t removed.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | ENROLLED_AFTER_REQUIRED | mode: "after" without a valid time. |
| 400 | KEEP_FIRST_REQUIRED | mode: "keep" without a whole number in keep_first. |
| 400 | DRIP_REMOVE_UNSENT_CURSOR | after or through isn’t a whole number, or only one of them was sent. |
#Delete a drip campaign
Deletes the drip, its steps and every contact’s place in it, all at once or not at all. Nobody gets another text from it. It also deletes the automation the app made to add contacts to this drip; your own automations that add contacts to it are kept. It can’t be undone. Needs the send_sms permission, and a team member must be the owner or an admin. Returns {"success": true}.
Parameters
idinteger, pathRequired- The drip campaign id.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_field | The id isn’t a whole number. |
| 403 | DRIP_DELETE_OWNER_ONLY | Your team’s drip, and you aren’t the owner or an admin. |
| 404 | DRIP_NOT_FOUND | No such drip, or it was already deleted. |
| 500 | DRIP_DELETE_FAILED | Nothing was changed. Try again. |
| 500 | DRIP_DELETE_OUTCOME_UNKNOWN | We couldn’t confirm it. List your drips before you try again. |
#Stop a contact’s drip
Takes one contact out of a drip, or out of every drip they are in, even after they were texted. No more steps are sent to them. It works the same as the Remove from drip campaign step in an automation. Needs the send_sms permission and a live key. The path starts with /zapier because the Zapier and n8n apps call it too.
Parameters
contact_idinteger- The contact. Send this or
phone. phonestring- The contact’s phone number, used when there is no
contact_id. It must match the saved number exactly once both are formatted. sequence_idinteger or"all"Default"all"- The drip campaign id (sequence is our older name for it). Send
"all"or leave it out to stop every drip the contact is in.
Returns
data.contact_idinteger- The contact.
data.sequence_idinteger or"all"- What was stopped.
data.stoppedinteger- How many enrollments were stopped.
0when the contact was not in a running drip, so it is safe to call again. A finished drip is left as it is.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | No contact_id or phone, or sequence_id isn’t a whole number or "all". | |
| 404 | No contact on your account matches, or the drip campaign isn’t yours. | |
| 500 | The stop failed and nothing changed. Try again. |
#Step types
| step_type | What it does | Fields |
|---|---|---|
send_sms | Sends a text, charged like any drip text. | message_body is the text, with merge fields like {{contact.first_name}}. Optional media_url (a public link) makes it a picture message. When you save the steps, a step that leaves out media_url keeps the picture stored at its position, so send "media_url": null for a step with no picture. Optional allow_empty_merge: true sends it even when a merge field is blank for a contact; without it, that contact skips the step. |
if_else | Checks the contact and jumps. | then_step_order when it matches, else_step_order when not. One condition is condition_field, condition_op and condition_value; several are conditions, a list of {field, op, value} with condition_combinator all or any. op is equals, not_equals, contains, not_contains, exists, not_exists, gt, gte, lt or lte. A field is a contact field, tags, lead_score, days_since_contact, reply_text (their last reply) or custom:<name>. conditions may also be a {match, conds} filter, the form the app’s builder saves. |
wait_reply | Waits for the contact to reply, then jumps by what they said. | reply_other_step_order is where any reply goes, and timeout_step_order after wait_timeout_minutes with no reply. To send a yes or a stop or a no somewhere else, add reply_yes_step_order or reply_stop_step_order; a yes with no yes step takes reply_other_step_order, and a stop or a no with no stop step ends the drip. With no timeout it waits 90 days, then the drip is finished for them. Optional neg_keywords, comma separated, adds words that count as a stop. Optional pos_keywords, comma separated, replaces the built-in yes words, so list every word that should count as yes. |
wait | Waits, then moves on. | wait_timeout_minutes, or its delay_* fields. |
add_tag | Tags the contact. | message_body is the tag. |
move_pipeline | Moves the contact to a pipeline stage. | message_body is the stage id. |
assign_user | Assigns the contact to a team member. | message_body is their user id. |
convo_status | Changes the conversation in your inbox. | message_body is a comma separated list of read, unread, archive, unarchive, star, unstar. |
notify | Alerts your team. | message_body is JSON {"channel", "text"}, channel being inapp, email or sms. An sms alert with "to" texts that number, which must be a phone on your team; without it, your own phone. Plain text is an in-app alert. |
webhook | Sends the contact to a web address. Never holds up the drip. | message_body is the URL, or JSON {"url", "method", "headers", "custom"}. Only public https:// or http:// addresses. |
end | Finishes the drip for the contact. Put one at the end of each branch, or the contact runs on into the next step. | None. |
A branch field left empty, or set to 0, finishes the drip for the contact when it is taken. On a wait_reply, a yes with no yes branch takes the other branch. A save is refused, with step_index (counting from 0) naming the step, when:
| HTTP | Code | When |
|---|---|---|
| 400 | DRIP_STEP_TYPE_INVALID | A step_type that isn’t one of these. |
| 400 | DRIP_STEP_INVALID | A step that isn’t an object. |
| 400 | DRIP_GRAPH_LOOP | The branches loop back to an earlier step. |
| 400 | DRIP_STEP_EMPTY | A send_sms step with no text has another step after it. One at the very end is dropped instead. |
| 400 | DRIP_STEP_DELAY_INVALID | A wait below 0 or over 10 years. |
| 400 | DRIP_STEP_BODY_INVALID | message_body isn’t text. |
| 400 | no code | A media_url or webhook address we can’t use. error says why. Also when every step is empty. |
| 400 | CONTENT_BLOCKED | A text breaks our content rules. |
See also: the guide to doing this in the app.