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.
Enroll a contact import
Section titled “Enroll a contact import”POST /campaigns/:campaign_id/enrollAuthorization: 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" } ]}Enroll a single contact
Section titled “Enroll a single contact”POST /campaigns/:campaign_id/enroll_contactAuthorization: Bearer <key>Content-Type: application/json
{ "contact_id": "<id>", "batch_number": 1, "priority": true }| Field | Required | Notes |
|---|---|---|
contact_id | yes | Must be in the same account. |
batch_number | no (default 0) | Place this enrollment in a specific batch. |
priority | no | When 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" }List a campaign’s enrollments
Section titled “List a campaign’s enrollments”GET /campaigns/:campaign_id/enrollments?status=sent&batch_number=1Filterable 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 }]Status transitions
Section titled “Status transitions”pending → sent → opened → replied ↓ ↓ ↓ └─────┴────────┴─→ bouncedpending → 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.
Replies & skips audit
Section titled “Replies & skips audit”GET /campaigns/:campaign_id/replies?limit=100GET /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.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
| 404 | campaign_not_found | Unknown campaign or another account’s. |
| 404 | contact_not_found | enroll_contact with unknown contact_id. |
| 404 | contact_import_not_found | enroll with unknown contact_import_id. |
| 422 | contact_already_enrolled | enroll_contact for an already-enrolled contact. |