Follow-ups API
Automated follow-up sequences, date reminders and enrollment rules. Product behaviour is described in the
follow-ups guide. Requires a plan with follow-ups (FEATURE_NOT_AVAILABLE otherwise).
| Scope | Allows |
|---|---|
followups:read | Read sequences, enrollments, rules, settings and statistics |
followups:enroll | Start, reschedule, pause, resume and cancel enrollments (one-off follow-ups included) |
followups:write | Create and change sequences, rules, settings and bulk enrollments |
Enroll a contact
POST /v1/followups/enrollments
Idempotency-Key: invoice-551-reminders
Content-Type: application/json
{
"sequenceId": "fus_01jq8x7y2z3a4b5c6d7e8f9g0h",
"contact": { "phone": "+923001234567", "displayName": "Sara Khan" },
"anchorAt": "2026-10-25T10:00:00+05:00",
"timezone": "Asia/Karachi",
"variables": { "invoiceNo": "INV-551", "amount": "PKR 12,000" },
"externalReference": "invoice-551",
"dedupeKey": "invoice-551-reminders"
}
- Pass one of
sequenceId(a published sequence) orinline(a one-off follow-up of up to 3 steps, same step format as sequences). - Pass one of
contactId,contact(found or created by phone number; needscontacts:write) orconversationId(sets the contact and the number; needsconversations:read). anchorAtis required when a step runs relative to the reminder date.dedupeKey: enrolling again with the same key returns the existing enrollment ("created": false), forever — use it for business events (an invoice, a booking).Idempotency-Keyprotects retries of one request (24 hours).instanceIdoverrides the sequence's choice of number.
Response 201: { "enrollment": FollowUpEnrollment, "created": true }.
Errors: FOLLOWUP_SEQUENCE_NOT_ACTIVE (draft, paused or archived), FOLLOWUP_ALREADY_ENROLLED (per the sequence's
re-entry policy), CONTACT_OPTED_OUT, FOLLOWUP_LIMIT_REACHED (running enrollments of the plan), VALIDATION_ERROR
(for example a reminder without anchorAt).
Manage enrollments
| Method | Path | |
|---|---|---|
GET | /v1/followups/enrollments | Filters: sequenceId, status, contactId, conversationId, externalReference, source |
GET | /v1/followups/enrollments/{id} | With runs: the history of each step and its message status |
PATCH | /v1/followups/enrollments/{id} | anchorAt (reschedule), timezone, variables (replaces them) |
POST | /v1/followups/enrollments/{id}/pause · /resume · /cancel | Cancel stops it with reason manual and cancels a follow-up message still waiting in the send queue |
POST | /v1/followups/enrollments/bulk | sequenceId and contactIds (up to 1,000) or segmentId; returns a job (202) |
GET | /v1/followups/enrollments/bulk/{jobId} | Progress: processed, enrolled, skipped |
Enrollment status: active, paused (pauseReason: user, sequence_paused, instance_unavailable),
completed, stopped (stopReason: replied, human_replied, opted_out, manual, lead_closed,
conversation_resolved, contact_deleted, instance_deleted, sequence_archived, superseded, plan_changed),
failed.
Conversations
POST /v1/conversations/{conversationId}/followups
{ "quick": { "type": "message", "afterSeconds": 86400, "text": "Hi {{contact.firstName | \"there\"}}, any questions about the quote?" } }
quick.type: message (sent if the contact has not replied) or reminder (notifies the assignee, else you).
Or pass sequenceId. GET on the same path lists the contact's follow-ups.
Sequences
| Method | Path | |
|---|---|---|
GET / POST | /v1/followups/sequences | List (filters status, kind) / create a draft (name, kind: no_reply or reminder, optional definition) |
GET / PATCH / DELETE | /v1/followups/sequences/{id} | PATCH edits the draft (pass version to detect concurrent edits); DELETE only before the first publish |
POST | /v1/followups/sequences/{id}/publish | Snapshot the draft into a new version; a draft becomes active |
POST | /v1/followups/sequences/{id}/pause · /resume · /archive | Archive takes { "stopEnrollments": true } to stop running enrollments |
GET | /v1/followups/sequences/{id}/stats | Outcomes, reply rate and a per-step funnel |
POST | /v1/followups/sequences/render-preview | Render a message with sample or given values |
A definition:
{
"steps": [
{
"id": "nudge1",
"wait": { "anchor": "enrollment", "offsetSeconds": 86400 },
"onlyIf": { "noReply": true },
"action": { "type": "send_message", "message": { "type": "text", "content": { "text": "Hi {{contact.firstName | \"there\"}}, did you see our quote?" } } }
},
{
"id": "nudge2",
"wait": { "anchor": "previous_step", "offsetSeconds": 172800 },
"action": {
"type": "send_message",
"message": { "type": "text", "content": { "text": "Happy to answer any question about the quote." } },
"ai": { "enabled": true, "instructions": "Politely ask if they are still interested.", "maxChars": 400, "onFailure": "template" }
}
},
{ "id": "owner", "wait": { "anchor": "previous_step", "offsetSeconds": 86400 }, "action": { "type": "notify", "to": "lead_owner", "text": "Call them" } }
],
"settings": {
"stopOn": { "reply": true, "humanMessage": true, "conversationResolved": false, "leadClosed": true },
"sendWindow": "project",
"consentMode": null,
"instance": "conversation",
"instanceId": null,
"pastDueGraceSeconds": 3600,
"messageTtlSeconds": null,
"reentry": "after_finish"
}
}
wait.anchor:enrollment,previous_steporanchor_date(reminders only;offsetSecondsmay then be negative:-86400is one day before).action.type:send_message(messageas in campaigns, optionalai),notify(to:assignee,lead_owneroruserwithuserId;text),tag(add,remove: tag ids),resolve_conversation.- Variables: contact and attribute variables,
{{vars.<key>}}and{{anchor.date|time|datetime|weekday}}. settings.instance:conversation(the contact's last number, else the default),project_defaultorfixed.
Rules
GET/POST /v1/followups/rules, GET/PATCH/DELETE /v1/followups/rules/{id}:
{
"name": "Remind admissions enquiries",
"sequenceId": "fus_…",
"trigger": { "type": "inbound_keyword", "keywords": ["admission", "fees"], "match": "contains" },
"conditions": { "type": "group", "op": "and", "rules": [{ "type": "tag", "op": "has_none", "tagIds": ["tag_enrolled_students"] }] },
"anchor": { "source": "none" },
"enabled": true
}
Triggers: contact_created, tag_added (tagId), inbound_keyword (keywords, match: whole or contains),
conversation_resolved, lead_created, lead_status_changed (optional status), deal_stage_changed (optional
pipelineId, stageId). conditions is a segment rule. Reminder sequences need an anchor: trigger_time or
contact_attribute (key of an attribute holding an ISO date or a Unix timestamp). Contacts created by CSV imports
do not fire rules; use a bulk enrollment instead.
Settings
GET/PUT /v1/followups/settings: timezone, sendWindow (enabled, days[] of {day, open, close},
holidays[]), maxPerContactPerDay, sendDelayMs, consentMode (opt_out default, or opt_in),
defaultInstanceId, defaultMessageTtlSeconds, retentionDays (finished enrollments are deleted after it).
Webhooks
| Event | When |
|---|---|
followup.enrolled | A contact entered a sequence or a one-off follow-up |
followup.step_queued | A follow-up message was queued (its message.* events follow) |
followup.completed | All steps ran |
followup.stopped | Stopped; reason is the stop reason |
followup.failed | Gave up after repeated internal errors |
Payload (data): enrollmentId, sequenceId, contactId, phone, status, stepIndex, stepId, messageId,
reason, source, externalReference, occurredAt.