Skip to content

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).

ScopeAllows
followups:readRead sequences, enrollments, rules, settings and statistics
followups:enrollStart, reschedule, pause, resume and cancel enrollments (one-off follow-ups included)
followups:writeCreate 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) or inline (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; needs contacts:write) or conversationId (sets the contact and the number; needs conversations:read).
  • anchorAt is 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-Key protects retries of one request (24 hours).
  • instanceId overrides 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

MethodPath
GET/v1/followups/enrollmentsFilters: 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 · /cancelCancel stops it with reason manual and cancels a follow-up message still waiting in the send queue
POST/v1/followups/enrollments/bulksequenceId 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

MethodPath
GET / POST/v1/followups/sequencesList (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}/publishSnapshot the draft into a new version; a draft becomes active
POST/v1/followups/sequences/{id}/pause · /resume · /archiveArchive takes { "stopEnrollments": true } to stop running enrollments
GET/v1/followups/sequences/{id}/statsOutcomes, reply rate and a per-step funnel
POST/v1/followups/sequences/render-previewRender 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_step or anchor_date (reminders only; offsetSeconds may then be negative: -86400 is one day before).
  • action.type: send_message (message as in campaigns, optional ai), notify (to: assignee, lead_owner or user with userId; 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_default or fixed.

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

EventWhen
followup.enrolledA contact entered a sequence or a one-off follow-up
followup.step_queuedA follow-up message was queued (its message.* events follow)
followup.completedAll steps ran
followup.stoppedStopped; reason is the stop reason
followup.failedGave up after repeated internal errors

Payload (data): enrollmentId, sequenceId, contactId, phone, status, stepIndex, stepId, messageId, reason, source, externalReference, occurredAt.