Send your first campaign
Goal: create and activate a marketing template, build a one-time email blast against a list or segment, preview and conflict-check it, launch it now or on a schedule, and follow its results.
Prerequisites: a verified sending domain (see Verify a sending domain); at least one list or segment with subscribed contacts (see Import contacts and Segment your audience); credits on your account; an API key with the templates:write, campaigns:read, campaigns:write and email:send scopes; Campaigns and Templates for the lifecycles used below.
Steps
1. Create a marketing template
A marketing template must contain an unsubscribe link, {{unsubscribe_url}} — without it the template cannot be activated.
curl -X POST https://emails.routeon.io/api/v1/templates
-H "Authorization: Bearer <API_KEY>"
-H "Content-Type: application/json"
-d '{
"name": "Monthly product update",
"format": "html",
"source": "<html><body><h1>Hi {{first_name}}</h1><p>Here is what is new this month.</p><p><a href="{{unsubscribe_url}}">Unsubscribe</a></p></body></html>",
"subject": "What is new this month",
"preheader": "Product updates and tips",
"message_class": "marketing"
}'
Expected response (201 Created):
{
"status": "success",
"http_code": 201,
"payload": {
"id": "<TEMPLATE_ID>",
"tenant_id": "<TENANT_ID>",
"name": "Monthly product update",
"current_version": 1,
"status": "draft",
"strict_variables": false,
"message_class": "marketing",
"created_at": "2026-09-28T09:00:00Z",
"updated_at": "2026-09-28T09:00:00Z"
},
"meta": {}
}
message_class defaults to marketing. A campaign needs a marketing template; transactional and critical templates are for transactional sends and OTP codes.
2. Verify the template content
curl -X POST https://emails.routeon.io/api/v1/templates/<TEMPLATE_ID>/verify
-H "Authorization: Bearer <API_KEY>"
-H "Content-Type: application/json"
-d '{ "version": 1 }'
The call answers with a 2xx status and a payload like:
{ "status": "approved", "reason": null }
pending means there is no verdict yet — call again shortly. rejected carries the flagged categories in reason; change the content and verify the new version.
3. Activate the template
curl -X PATCH https://emails.routeon.io/api/v1/templates/<TEMPLATE_ID>
-H "Authorization: Bearer <API_KEY>"
-H "Content-Type: application/json"
-d '{ "status": "active" }'
200 OK returns the template with status: "active". Without an approved verdict the call is 422 (template_not_verified). Any later content edit creates a new version and returns the template to draft — verify and activate again before launching with it.
4. Choose the sender (optional)
The From address is your verified domain plus a sender profile’s mailbox name. List your profiles:
curl https://emails.routeon.io/api/v1/senders
-H "Authorization: Bearer <API_KEY>"
Each profile has an id, a local_part (the part before @), a display_name, a reply_to and is_default. Pass a profile’s id as sender_profile_id in the next step, or leave it out to use the profile routed to campaigns, or your default profile. See Domains & warmup for sender profiles and routes.
5. Create the campaign
Target a list or a segment through audience.include; both use the same { "kind", "id" } shape:
curl -X POST https://emails.routeon.io/api/v1/campaigns
-H "Authorization: Bearer <API_KEY>"
-H "Content-Type: application/json"
-H "Idempotency-Key: monthly-update-create-0001"
-d '{
"name": "Monthly product update blast",
"type": "blast",
"domain_id": "<DOMAIN_ID>",
"template_id": "<TEMPLATE_ID>",
"audience": { "include": [ { "kind": "segment", "id": "<SEGMENT_ID>" } ] },
"sender_profile_id": "<SENDER_PROFILE_ID>",
"from_name": "Example Co",
"reply_to": "support@example.com"
}'
Expected response (201 Created):
{
"status": "success",
"http_code": 201,
"payload": {
"id": "<CAMPAIGN_ID>",
"name": "Monthly product update blast",
"type": "blast",
"status": "draft",
"domain_id": "<DOMAIN_ID>",
"template_id": "<TEMPLATE_ID>",
"template_version": 1,
"audience": { "include": [ { "kind": "segment", "id": "<SEGMENT_ID>" } ] },
"schedule_at": null,
"sender_profile_id": "<SENDER_PROFILE_ID>",
"from_name": "Example Co",
"reply_to": "support@example.com",
"stats": { "sent": 0, "delivered": 0, "opened": 0, "clicked": 0, "bounced": 0, "complained": 0, "failed": 0 },
"recipient_count": 0,
"throttle_per_hour": null,
"launched_at": null,
"completed_at": null,
"pause_reason": null,
"created_at": "2026-09-28T09:10:00Z",
"updated_at": "2026-09-28T09:10:00Z"
},
"meta": {}
}
- There is no
from_emailfield: the address comes from the sender profile and the domain.from_nameoverrides the profile’s display name. audience.excludesubtracts lists, segments or uploads from the result.kind: "upload"refers to an ad-hoc recipient file created withPOST /campaigns/uploads.throttle_per_houroptionally spreads the send over time."type": "automation"creates an automation instead (see Automations);"transactional"is refused.
6. Preview the audience
curl -X POST https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/preview
-H "Authorization: Bearer <API_KEY>"
Expected response (200 OK):
{
"status": "success",
"http_code": 200,
"payload": {
"recipient_count": 187,
"sample_emails": ["jane@example.com", "john@example.com"]
},
"meta": {}
}
This is the audience the campaign would send to now: include minus exclude, only subscribed contacts, minus suppressed addresses. It can be smaller than the segment preview, which counts every matching contact.
7. Check for same-day conflicts
Find recipients who would get more than one email on the campaign’s send day (in your account’s timezone), counting your other sends and campaigns:
curl -X POST https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/conflicts
-H "Authorization: Bearer <API_KEY>"
Expected response (200 OK):
{
"status": "success",
"http_code": 200,
"payload": {
"target_date": "2026-09-28",
"audience_size": 187,
"conflict_count": 12,
"by_source": [ { "name": "Welcome automation", "recipients": 12 } ],
"other_sources": 0
},
"meta": {}
}
If conflict_count is not zero, choose a conflict_resolution for the launch: send_anyway, exclude_conflicts, delay_1_day or spread_schedule.
8. Launch now, or schedule
Launch immediately:
curl -X POST https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/send
-H "Authorization: Bearer <API_KEY>"
-H "Content-Type: application/json"
-H "Idempotency-Key: monthly-update-launch-0001"
-d '{ "conflict_resolution": "exclude_conflicts" }'
Expected response (202 Accepted) — the campaign with status: "sending". Messages are created and sent in the background. If there are same-day conflicts and you left out conflict_resolution, the call is 409 with the conflict summary in meta.details (and no code); choose a resolution and call again.
Or schedule it instead:
curl -X POST https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/schedule
-H "Authorization: Bearer <API_KEY>"
-H "Content-Type: application/json"
-d '{
"schedule_at": "<FUTURE_ISO8601_TIMESTAMP>",
"conflict_resolution": "exclude_conflicts"
}'
200 OK returns the campaign with status: "scheduled" and schedule_at set; it launches by itself at that time. schedule_at must be an RFC 3339 timestamp with an offset, in the future (422 schedule_in_past otherwise — use /send to launch now).
Keep the template untouched while the campaign is scheduled or sending: a content edit returns the template to draft, and the campaign can then be refused at launch or paused.
9. Pause, resume, or cancel
curl -X POST https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/pause
-H "Authorization: Bearer <API_KEY>"
curl -X POST https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/resume
-H "Authorization: Bearer <API_KEY>"
curl -X POST https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/cancel
-H "Authorization: Bearer <API_KEY>"
Each returns 200 OK with the campaign in its new status (paused, sending, canceled). When the platform pauses a campaign itself, pause_reason says why:
pause_reason |
Meaning | What to do |
|---|---|---|
insufficient_credits |
Credits ran out mid-send | Top up, then resume |
credit_protection |
The send would cross your protected minimum balance | Top up, then resume |
template_mismatch |
The template changed after launch | Verify and activate the template, then resume |
sender_not_configured |
No sender profile resolves for the campaign | Fix the sender profile or route, then resume |
null |
Paused by you, or no recorded reason | Resume when ready |
10. Read the results
curl https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>
-H "Authorization: Bearer <API_KEY>"
payload.stats has the running totals. For a day-by-day series:
curl "https://emails.routeon.io/api/v1/campaigns/<CAMPAIGN_ID>/series?range=7d"
-H "Authorization: Bearer <API_KEY>"
Expected response (200 OK):
{
"status": "success",
"http_code": 200,
"payload": {
"range": "7d",
"totals": { "sent": 187, "delivered": 181, "opened": 84, "clicked": 19, "bounced": 4, "complained": 0, "failed": 2 },
"series": [
{ "date": "2026-09-28", "sent": 187, "delivered": 181, "opened": 84, "clicked": 19, "bounced": 4, "complained": 0, "failed": 2 }
]
},
"meta": {}
}
range is 7d (default), 30d, or all (from the campaign’s creation through today).
Verify
GET /campaigns/{id} reaches status: "sent" with completed_at set once the blast has gone out, and stats / GET /campaigns/{id}/series show delivered, opened, clicked, bounced, complained and failed counts. For per-message detail, use GET /messages?campaign_id=<CAMPAIGN_ID>.
Common failures
| Symptom | Cause | Fix |
|---|---|---|
422 on POST /campaigns mentioning from_email |
from_email is not a campaign field |
Remove it; use sender_profile_id and from_name |
422 template_class_mismatch on create |
The template is not marketing |
Use a marketing template |
422 sender_not_found |
sender_profile_id is not one of your profiles |
Pick an id from GET /senders |
422 template_not_active |
Template not activated, or edited back to draft |
Verify and activate it (steps 2–3) |
422 domain_not_verified |
domain_id is not a verified domain of your account |
Finish Verify a sending domain |
422 invalid_variable |
Data for the template does not fit one of its declared variables | Fix the data or the template’s variable declaration |
422 empty_audience on launch |
No recipients remain after exclusions, statuses and suppressions | Check POST /campaigns/{id}/preview; widen the audience |
422 schedule_in_past |
schedule_at is not in the future |
Use a future time, or /send to launch now |
409 with a code on /send |
invalid_state (not in a launchable state), or a content or account review block: tenant_suspended, tenant_needs_moderation, campaign_content_rejected, campaign_needs_content_review, template_changed_since_approval |
Resolve the named cause; for template_changed_since_approval, verify and activate the template again |
409 without a code on /send |
Same-day conflicts and no conflict_resolution |
Retry with a resolution from step 7 |
402 on /send |
Not enough credits, or the send would cross your protected minimum | meta.details shows the shortfall; top up |
409 invalid_state on DELETE /campaigns/{id} |
Only a draft can be deleted |
Use POST /campaigns/{id}/cancel |
| Fewer recipients than the segment size | Unsubscribed, bounced, complained and suppressed contacts are not sent to | Expected — POST /campaigns/{id}/preview shows the real count |