Campaigns
A Campaign is the unit of multi-step outreach. It owns its sequence steps, SMTP credentials, and enrollments.
List campaigns
Section titled “List campaigns”GET /campaignsAuthorization: Bearer <key>[ { "id": "...", "name": "Q2 launch", "status": "active", "created_at": "..." }]Create a campaign
Section titled “Create a campaign”POST /campaignsAuthorization: Bearer <key>Content-Type: application/json
{ "campaign": { "name": "Q2 launch", "outreach_id": "<optional>", "min_wait_seconds": 120, "max_wait_seconds": 420, "verify_email_mx": true, "bounce_rate_threshold": 8.0, "smtp_credentials": [{ "label": "Primary", "smtp_host": "mail.smtp2go.com", "smtp_port": 587, "smtp_username": "...", "smtp_password": "...", "daily_limit": 200, "imap_host": "imap.gmail.com", "imap_port": 993, "imap_username": "...", "imap_password": "..." }], "sequence_steps": [ { "position": 1, "subject": "Hi {{first_name}}", "body_html": "<p>...</p>", "delay_minutes": 0 }, { "position": 2, "subject": "Re: Hi", "body_html": "<p>...</p>", "delay_minutes": 4320 } ] }}| Field | Required | Default | Notes |
|---|---|---|---|
name | yes | — | |
outreach_id | no | null | If set + outreach has unique_contacts: true, dedupes contacts across sibling campaigns. |
min_wait_seconds / max_wait_seconds | no | 120 / 420 | Random jitter between sends in a batch. |
verify_email_mx | no | false | DNS MX lookup at dispatch; bounces contacts with no MX record. |
bounce_rate_threshold | no | 8.0 | Per-credential auto-disable threshold (%). |
smtp_credentials | yes (≥1) | — | IMAP fields optional but enable reply detection. |
sequence_steps | yes (≥1) | — | Templates support {{first_name}}, {{last_name}}, {{email}}, {{company}}. |
{ "id": "...", "status": "pending" }Campaign starts as pending, transitions to active on first dispatch.
Show a campaign (full stats)
Section titled “Show a campaign (full stats)”GET /campaigns/:id{ "totals": { "pending": 100, "sent": 50, "opened": 20, "replied": 3, "bounced": 5 }, "rates": { "bounce_rate": 5.0, "open_rate": 40.0, "reply_rate": 6.0 }, "per_batch": [...], "recent_bounces": [...], "credentials": [...], "skipped": { "mx_invalid": 0, "cross_campaign_duplicate": 0 }}bounce_rate is per-send (matches SMTP2GO’s denominator).
Update / Delete
Section titled “Update / Delete”PATCH /campaigns/:id { "campaign": { "name": "...", "bounce_rate_threshold": 10.0 } }DELETE /campaigns/:idUpdatable: name, min_wait_seconds, max_wait_seconds, outreach_id, verify_email_mx, bounce_rate_threshold. DELETE cascades to sequence_steps, smtp_credentials, enrollments, enrollment_skips.
Workflow actions
Section titled “Workflow actions”| Action | Endpoint | Purpose |
|---|---|---|
| Enroll a contact import | POST /campaigns/:id/enroll | See Enrollments. |
| Enroll a single contact | POST /campaigns/:id/enroll_contact | Body: { contact_id, batch_number?, priority? }. |
| Dispatch a batch | POST /campaigns/:id/dispatch | Body: { batch_number } (or omit for next pending). |
| Test send | POST /campaigns/:id/test_send | Body: { emails: ["[email protected]"] }. Preview by sending all steps to yourself. |
| Preview HTML | GET /campaigns/:id/preview?contact_id=... | Rendered subject + body with merge tags applied. |
| Pause | POST /campaigns/:id/pause | Active → paused. Scheduled jobs return early when they fire. |
| Resume | POST /campaigns/:id/resume | Paused → active. |
| Send forecast | GET /campaigns/:id/send_forecast?days=7 | Daily-bucket forecast of scheduled SendEmailJobs. |
| Skips audit | GET /campaigns/:id/skips?kind=mx_invalid&limit=100 | Recently skipped enrollments. |
| Replies audit | GET /campaigns/:id/replies?limit=100 | Replied enrollments with hours-to-reply. |
| Re-enable a credential | POST /campaigns/:id/smtp_credentials/:credential_id/reenable | Clear disabled_at on an auto-disabled credential. |
Dispatch behavior
Section titled “Dispatch behavior”POST /campaigns/:id/dispatch schedules SendEmailJobs for every pending enrollment in the requested batch. Step 1 fires immediately (with jitter); step 2+ are scheduled at the cumulative delay.
The dispatcher refuses to enqueue when:
- Account over plan limit → 402
plan_limit_reached - All SMTP credentials at their daily_limit for today’s sends (step 2/3 don’t count against today)
- Campaign paused, or has zero credentials/steps
Use GET /campaigns/:id to track progress, or set up an enrollment.sent webhook for real-time signals.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
| 404 | campaign_not_found | Unknown id 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 | validation_failed | Model validation (e.g., name required). |
| 422 | no_pending_enrollments | dispatch with nothing pending. |
| 422 | campaign_not_active / campaign_not_paused | pause/resume on the wrong state. |
| 422 | contact_already_enrolled | enroll_contact for an already-enrolled contact. |
| 402 | plan_limit_reached | Account at plan monthly_limit. |