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_email field: the address comes from the sender profile and the domain. from_name overrides the profile’s display name.
  • audience.exclude subtracts lists, segments or uploads from the result. kind: "upload" refers to an ad-hoc recipient file created with POST /campaigns/uploads.
  • throttle_per_hour optionally 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