Skip to content

Enrollments

An Enrollment is the link between a Contact and a Campaign. It owns the per-contact state machine: pending → sent → opened → replied|bounced. The dispatcher schedules SendEmailJobs based on enrollment + step.

POST /campaigns/:campaign_id/enroll
Authorization: Bearer <key>
Content-Type: application/json
{ "contact_import_id": "<id>", "batch_size": 50 }

Bulk-enrolls every subscribed contact in the import that isn’t already enrolled. If the campaign belongs to an Outreach with unique_contacts: true, contacts already enrolled in a sibling campaign are skipped (and reported in skipped).

If verify_email_mx: true on the campaign, enrolled contacts whose domain has no MX record are skipped at enrollment time and recorded as enrollment_skips with kind: "mx_invalid".

{
"enrolled": 27,
"batches": 1,
"batch_size": 50,
"skipped": [
{ "contact_id": "...", "email": "...", "reason": "Already enrolled in another campaign in this outreach" }
]
}
POST /campaigns/:campaign_id/enroll_contact
Authorization: Bearer <key>
Content-Type: application/json
{ "contact_id": "<id>", "batch_number": 1, "priority": true }
FieldRequiredNotes
contact_idyesMust be in the same account.
batch_numberno (default 0)Place this enrollment in a specific batch.
prioritynoWhen truthy, sets created_at: 2000-01-01 so the dispatcher schedules this enrollment first within its batch.
{ "id": "...", "contact_id": "...", "batch_number": 1, "status": "pending" }
GET /campaigns/:campaign_id/enrollments?status=sent&batch_number=1

Filterable by status (pending, sent, opened, replied, bounced) and batch_number.

[
{
"id": "...",
"contact_id": "...",
"smtp_credential_id": "...",
"status": "sent",
"batch_number": 1,
"sent_at": "...",
"opened_at": "...",
"replied_at": null,
"bounce_reason": null
}
]
pending → sent → opened → replied
↓ ↓ ↓
└─────┴────────┴─→ bounced
  • pending → sent: SendEmailJob successfully delivered step 1.
  • sent → opened: tracking pixel was loaded.
  • opened → replied: ReplyDetectionJob matched an inbound message.
  • * → bounced: synchronous SMTP error or async SMTP2GO bounce webhook.

Each transition emits an outbound webhook (enrollment.sent, .opened, .replied, .bounced, .unsubscribed). See Webhooks.

GET /campaigns/:campaign_id/replies?limit=100
GET /campaigns/:campaign_id/skips?kind=mx_invalid&limit=100

/replies includes hours_to_reply for each enrollment. /skips returns enrollment_skip records with kind ∈ mx_invalid | cross_campaign_duplicate | already_enrolled.

StatusCodeWhen
404campaign_not_foundUnknown campaign or another account’s.
404contact_not_foundenroll_contact with unknown contact_id.
404contact_import_not_foundenroll with unknown contact_import_id.
422contact_already_enrolledenroll_contact for an already-enrolled contact.