Skip to main content

Nurture Campaigns

Nurture campaigns are multi-step outbound SMS sequences scoped per tenant. They are distinct from Drip Campaigns (the older Cal.com-shaped path). Use Nurture for speed-to-lead, missed-call text-back, and cold re-engagement. Replies stop the sequence and hand the conversation off to your AI chatbot when configured.

Triggers and enroll modes

Trigger types: manual, web_lead_form, vapi_call_no_booking, speed_to_lead_no_reply, st_new_booking.

Enroll modes:

  • cron: the enrollment is picked up on the next sender tick, which runs every 2 minutes.
  • immediate: step 1 fires inline at enrollment time.

Send windows: each campaign carries send_window_start_local and send_window_end_local (local times such as 08:00 and 20:00) that bound when steps may send.

Exit on reply: when exit_on_reply is true, an inbound reply exits the enrollment. An optional next_campaign_key chains the contact into a follow-on campaign.

Response shapes

Nurture endpoints return unwrapped objects (for example { "campaigns": [...] } or { "ok": true }), not the data/has_more envelope used by most other list endpoints. The shapes below are exact.

Campaigns

List campaigns

GET /api/v1/nurture/campaigns lists the authenticated tenant's nurture campaigns with step counts. Scope: read.

Response
{
"campaigns": [
{
"id": "c1af...",
"campaign_key": "speed_to_lead",
"name": "Speed-to-Lead",
"trigger_type": "web_lead_form",
"enroll_mode": "immediate",
"exit_on_reply": true,
"next_campaign_key": "cold_nurture",
"send_window_start_local": "08:00",
"send_window_end_local": "20:00",
"active": true,
"step_count": 2,
"created_at": "2026-04-29T18:00:00Z",
"updated_at": "2026-04-29T18:00:00Z"
}
]
}

Create a campaign

POST /api/v1/nurture/campaigns creates a new campaign with its initial step list. Scope: admin. Defaults to active: false so you can review templates before going live.

Request
{
"campaign_key": "estimate_follow_up",
"name": "Estimate Follow-Up",
"trigger_type": "manual",
"enroll_mode": "cron",
"exit_on_reply": true,
"send_window_start_local": "09:00",
"send_window_end_local": "18:00",
"active": false,
"steps": [
{ "step_number": 1, "delay_minutes_from_prior": 0, "message_template": "Hey {first_name}, this is {firm_name} following up on your estimate. Any questions?" },
{ "step_number": 2, "delay_minutes_from_prior": 4320, "message_template": "{first_name}, want to lock in a slot this week?" }
]
}
Response (201 Created)
{ "id": "c1af..." }

campaign_key must be lowercase letters, numbers, and underscores, starting with a letter, and unique per tenant. Returns 409 campaign_key_taken if the key already exists. Step templates support the {first_name} and {firm_name} variables.

Get a campaign

GET /api/v1/nurture/campaigns/:id fetches a single campaign with its ordered steps. Scope: read.

Response
{
"campaign": { "id": "c1af...", "name": "Speed-to-Lead", "active": true },
"steps": [
{ "id": "...", "step_number": 1, "delay_minutes_from_prior": 0, "message_template": "..." },
{ "id": "...", "step_number": 2, "delay_minutes_from_prior": 60, "message_template": "..." }
]
}

Update a campaign

PATCH /api/v1/nurture/campaigns/:id updates editable fields and optionally replaces the step list. Scope: admin. All fields are optional; send only what you want to change.

Request
{
"active": true,
"name": "Speed-to-Lead v2",
"exit_on_reply": true,
"next_campaign_key": "cold_nurture",
"send_window_start_local": "08:00",
"send_window_end_local": "20:00",
"steps": [
{ "step_number": 1, "delay_minutes_from_prior": 0, "message_template": "Hey {first_name}, got your request..." },
{ "step_number": 2, "delay_minutes_from_prior": 60, "message_template": "Quick check-in..." }
]
}

When steps is provided, the entire step list is replaced atomically (delete + insert). For active campaigns, edit copy with care: in-flight enrollments keep their current_step integer.

Response
{ "ok": true }

Delete a campaign

DELETE /api/v1/nurture/campaigns/:id deletes a campaign and cascades to its steps, enrollments, and sends. Scope: admin.

By default the delete is refused if any enrollments are currently active (returns 409 has_active_enrollments). Pass ?force=1 to delete anyway; the cascading deletes cancel every active enrollment.

Response
{ "ok": true }

Enrollments

List enrollments

GET /api/v1/nurture/enrollments returns a paginated list of enrollments for the authenticated tenant. Scope: read.

ParameterDescription
statusactive, replied, completed, exited, or paused
campaign_idFilter to one campaign
limitMax 200, default 50
offsetOffset pagination
Response
{
"enrollments": [
{
"id": "e1...",
"campaign_id": "c1af...",
"conversation_id": "conv1...",
"recipient_phone": "+14075551234",
"recipient_name": "Jane Doe",
"current_step": 1,
"status": "active",
"enrolled_at": "2026-04-29T18:23:23Z",
"next_send_at": "2026-04-29T19:23:23Z",
"last_sent_at": "2026-04-29T18:23:24Z",
"exited_reason": null,
"metadata": { "source": "vapi_call_no_booking", "call_id": "..." },
"nurture_campaigns": { "campaign_key": "missed_call_textback", "name": "Missed-Call Text-Back" }
}
],
"total": 42,
"limit": 50,
"offset": 0
}

Enroll a phone number

POST /api/v1/nurture/enrollments manually enrolls a phone number in a campaign. Scope: send_sms. Idempotent: it refuses duplicate active enrollments per (campaign, phone) pair.

Request
{
"campaign_key": "speed_to_lead",
"phone": "+14075551234",
"name": "Jane Doe"
}

Pass either campaign_id (UUID) or campaign_key (slug); both work.

Response
{
"enrolled": true,
"enrollment_id": "e1...",
"reason": "enrolled",
"first_step_sent": true
}

For enroll_mode: "immediate" campaigns, step 1 fires inline and first_step_sent reflects whether the SMS landed at the carrier. For cron campaigns, the next sender tick (every 2 minutes) handles it. Returns enrolled: false with a reason when the recipient is already actively enrolled.

Get an enrollment

GET /api/v1/nurture/enrollments/:id fetches a single enrollment with its full step-send history. Scope: read.

Response
{
"enrollment": { "id": "e1...", "status": "active", "current_step": 1 },
"sends": [
{ "id": "s1", "step_number": 1, "rendered_message": "Hey Jane, ...", "outcome": "sent", "sent_at": "2026-04-29T18:23:24Z" }
]
}

Pause, resume, or cancel an enrollment

PATCH /api/v1/nurture/enrollments/:id transitions a single enrollment. Scope: admin.

Request
{ "action": "pause" }
Response
{ "ok": true, "action": "pause" }
  • pause requires status active.
  • resume requires status paused (resets next_send_at to now if it was in the past).
  • cancel works on active or paused; it rejects terminal statuses (completed, replied, exited).