Project API

Campaigns API

Create marketing campaigns and send now or schedule for later.

View as Markdown ↗

Project key. Acts in the project the key belongs to. An all-access key works too — name the project with an X-Project-Id header. How keys and scopes work.

Endpoints

GET/v1/campaigns
POST/v1/campaigns
GET/v1/campaigns/{id}
PATCH/v1/campaigns/{id}
DELETE/v1/campaigns/{id}
POST/v1/campaigns/{id}/send
POST/v1/campaigns/{id}/variants
PATCH/v1/campaigns/{id}/variants/{variant_id}
DELETE/v1/campaigns/{id}/variants/{variant_id}
GET/v1/campaigns/{id}/ab
POST/v1/campaigns/{id}/ab/winner

Create a campaign

A campaign is one marketing email. Pick an audience — a list, a segment, or a single contact for a one-off email — and content from raw html or a saved template.

Body parameters

namestringrequired

Internal name for the campaign.

subjectstringoptional

Subject line (personalizable). Falls back to the template's subject if a template_id is given.

list_idstringoptional

Audience: send to every confirmed, subscribed contact on this list (double-opt-in pending members are excluded).

segment_idstringoptional

Audience: send to contacts matching this segment (evaluated live).

contact_idstringoptional

Audience: send to a single contact — a one-off marketing email.

htmlstringoptional

Content: your HTML email. Inline CSS and <style> blocks are preserved. Personalize with {{ contact.first_name }}, with a fallback via {{ contact.first_name | default: "there" }}. Missing fields render empty.

template_idstringoptional

Content: render a saved template (alternative to html).

content_json / theme_jsonobjectoptional

Content: the document the dashboard's visual editor produces, and its theme. Accepted so a campaign drafted in the editor can be updated over the API — send html or template_id if you're composing from code.

from_emailstringoptional

Sender address. Defaults to your shared sending domain.

from_namestringoptional

Sender display name.

preview_textstringoptional

Inbox preview text shown after the subject.

Send to a list (raw HTML)
curl -X POST https://emailbump.com/api/v1/campaigns \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "July sale",
    "subject": "50% off, {{ contact.first_name | default: \"friend\" }}",
    "from_name": "Acme",
    "list_id": "5b3a0ea3-5c58-423e-92ff-0aaac7be0af3",
    "html": "<div style=\"font-family:sans-serif\"><h1>Summer sale</h1><p>Hi {{ contact.first_name | default: \"there\" }}, everything is 50% off this week.</p></div>"
  }'
One-off to a single contact
curl -X POST https://emailbump.com/api/v1/campaigns \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Personal note",
    "subject": "A quick hello",
    "contact_id": "5fef0867-b292-46ca-8371-e24d8575576c",
    "html": "<p>Hi {{ contact.first_name }}, just checking in!</p>"
  }'

Send or schedule

POST/v1/campaigns/{id}/send

One endpoint for both: omit the body to send now, or pass scheduled_at to send at a future time.

Body parameters

scheduled_atstringoptional

RFC 3339 timestamp, must be in the future. Omit to send immediately.

send_strategystringoptional

fixed (default, everyone at once), gradual (release in batches), or smart (each recipient at the hour they usually engage). smart is locked until the account has 100 human opens or clicks, from 50 distinct contacts, across 2+ campaigns or flows, in the last 180 days. Neither gradual nor smart is available on an A/B test.

batch_percentnumberoptional

Gradual sends: share of the audience per batch, 1–50. Defaults to 10.

batch_intervalstringoptional

Gradual sends: minute or hour between batches.

timezone_modestringoptional

specific (default) sends at one instant; local sends at the same wall-clock time in each recipient’s own timezone.

send_timezonestringoptional

IANA zone, e.g. America/New_York. With timezone_mode: "local" it’s the zone whose wall-clock reading of scheduled_at is replayed for each recipient; it’s also the fallback for recipients whose own timezone is unknown. It never shifts a specific send — scheduled_at is already absolute. Defaults to your account timezone. Send null to clear it.

local_past_behaviorstringoptional

Local-timezone sends, for recipients whose time already passed: next_day (default) or immediate.

recipients_at_send_timebooleanoptional

Snapshot the audience when the campaign sends instead of when it’s scheduled. Not available with timezone_mode: "local".

cURL
# Send now
curl -X POST https://emailbump.com/api/v1/campaigns/CAMPAIGN_ID/send \
  -H "Authorization: Bearer ebk_your_key"

# …or schedule for later
curl -X POST https://emailbump.com/api/v1/campaigns/CAMPAIGN_ID/send \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "scheduled_at": "2026-08-01T09:00:00Z" }'

# …or roll it out gradually, 10% every hour, in each recipient's timezone
curl -X POST https://emailbump.com/api/v1/campaigns/CAMPAIGN_ID/send \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduled_at": "2026-08-01T09:00:00Z",
    "send_strategy": "gradual",
    "batch_percent": 10,
    "batch_interval": "hour",
    "timezone_mode": "local",
    "local_past_behavior": "next_day"
  }'
200 (send now)
{ "status": "sending", "recipients": 1240 }
200 (scheduled)
{
  "status": "scheduled",
  "campaign": { "id": "…", "status": "scheduled" },
  "plan": {
    "total": 1240,
    "batches": 10,
    "first_send_at": "2026-07-31T23:00:00Z",
    "last_send_at": "2026-08-01T22:00:00Z",
    "unknown_timezone": 12,
    "timezones": 18
  }
}

A/B testing

Send 2–4 variations of a campaign and either let the winner go out automatically (find a winner) or split the whole audience between them (simple split, ab_test_percent: 100). Each variant overrides the base campaign’s subject, preview text, or content; anything you don’t override is inherited.

A/B fields (on create or PATCH)

ab_enabledbooleanoptional

Turn A/B testing on. Enabling without variants seeds an identical A/B pair to edit.

ab_test_percentnumberoptional

Share of the audience in the test, 5–100. Under 100 the remainder is held back for the winner; 100 splits the whole audience with no winner send.

ab_winner_metricstringoptional

open_rate (default, best for subject lines) or click_rate (best for content). Unique, human-only engagement.

ab_wait_minutesnumberoptional

How long after the test batch finishes before the winner is decided, 30–10080. Default 360 (6 hours).

variantsarrayoptional

Create only: 2–4 variant objects, each with optional label, subject, preview_text, html, content_json/theme_json, and split_percent (1–99; set on every variant and summing to 100, e.g. 60/40 — omit everywhere for an even split).

Create a subject-line test (winner by open rate after 4h)
curl -X POST https://emailbump.com/api/v1/campaigns \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "July sale (A/B)",
    "list_id": "5b3a0ea3-5c58-423e-92ff-0aaac7be0af3",
    "html": "<p>Everything is 50% off this week.</p>",
    "ab_enabled": true,
    "ab_test_percent": 20,
    "ab_winner_metric": "open_rate",
    "ab_wait_minutes": 240,
    "variants": [
      { "subject": "50% off everything this week" },
      { "subject": "Your summer discount is inside" }
    ]
  }'
Full-list 60/40 split (no winner send)
curl -X POST https://emailbump.com/api/v1/campaigns \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Newsletter redesign split",
    "list_id": "5b3a0ea3-5c58-423e-92ff-0aaac7be0af3",
    "html": "<p>The classic layout.</p>",
    "ab_enabled": true,
    "ab_test_percent": 100,
    "variants": [
      { "subject": "This week at Acme", "split_percent": 60 },
      { "subject": "This week at Acme", "split_percent": 40,
        "html": "<p>The bold redesign.</p>" }
    ]
  }'
An A/B send is a fixed send

The held-back remainder has no release time until the winner is picked, which can’t be reconciled with gradual pacing or per-recipient local timezones — POST …/send refuses both. These rules, and split_percent totalling 100, are checked when you send rather than when you create, so a campaign with 60/30 splits is accepted at create and refused at send.

After sending, poll GET /v1/campaigns/{id}/ab for per-variant results. While the test is running (status sending or testing) you can end it early with POST /v1/campaigns/{id}/ab/winner.

GET /v1/campaigns/{id}/ab — 200
{
  "enabled": true,
  "status": "testing",
  "test_percent": 20,
  "winner_metric": "open_rate",
  "test_ends_at": "2026-08-01T13:00:00Z",
  "winner_variant_id": null,
  "held": 992,
  "variants": [
    { "id": "…", "label": "A", "subject": "50% off everything this week",
      "sent": 124, "delivered": 123, "opens": 61, "clicks": 18,
      "open_rate": 0.4959, "click_rate": 0.1463, "is_winner": false },
    { "id": "…", "label": "B", "subject": "Your summer discount is inside",
      "sent": 124, "delivered": 124, "opens": 48, "clicks": 12,
      "open_rate": 0.3871, "click_rate": 0.0968, "is_winner": false }
  ]
}
Choose the winner early
curl -X POST https://emailbump.com/api/v1/campaigns/CAMPAIGN_ID/ab/winner \
  -H "Authorization: Bearer ebk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "variant_id": "VARIANT_ID" }'

Delete a campaign

DELETE/v1/campaigns/{id}

Removes a campaign that never sent, with its recipient rows and its A/B variants. Returns 204. It is for clearing out drafts and mistakes — anything that has actually reached someone is permanent.

cURL
curl -X DELETE https://emailbump.com/api/v1/campaigns/CAMPAIGN_ID \
  -H "Authorization: Bearer ebk_your_key"
Two refusals, both 409

A campaign that is going out mustn’t be called off silently, so sending, scheduled and testing are refused until you pause or cancel (an A/B test in flight has no pause — cancel it). A campaign that has gone out is refused permanently: delivery events carry a campaign id but survive the campaign, so deleting it would leave every open, click and bounce it produced counted in your totals with nothing to attribute them to.

409 — still going out
{
  "error": "This campaign is scheduled, so deleting it would call off a send without saying so. Pause it first so it stops going out, or cancel it, then delete it."
}
409 — already sent, permanently
{
  "error": "This campaign has already sent, so it can't be deleted — its opens, clicks and bounces would stay in your reports with nothing to attribute them to. Only a campaign that never sent can be removed."
}