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
/v1/campaigns/v1/campaigns/v1/campaigns/{id}/v1/campaigns/{id}/v1/campaigns/{id}/v1/campaigns/{id}/send/v1/campaigns/{id}/variants/v1/campaigns/{id}/variants/{variant_id}/v1/campaigns/{id}/variants/{variant_id}/v1/campaigns/{id}/ab/v1/campaigns/{id}/ab/winnerCreate 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
namestringrequiredInternal name for the campaign.
subjectstringoptionalSubject line (personalizable). Falls back to the template's subject if a template_id is given.
list_idstringoptionalAudience: send to every confirmed, subscribed contact on this list (double-opt-in pending members are excluded).
segment_idstringoptionalAudience: send to contacts matching this segment (evaluated live).
contact_idstringoptionalAudience: send to a single contact — a one-off marketing email.
htmlstringoptionalContent: 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_idstringoptionalContent: render a saved template (alternative to html).
content_json / theme_jsonobjectoptionalContent: 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_emailstringoptionalSender address. Defaults to your shared sending domain.
from_namestringoptionalSender display name.
preview_textstringoptionalInbox preview text shown after the subject.
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>"
}'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
/v1/campaigns/{id}/sendOne endpoint for both: omit the body to send now, or pass scheduled_at to send at a future time.
Body parameters
scheduled_atstringoptionalRFC 3339 timestamp, must be in the future. Omit to send immediately.
send_strategystringoptionalfixed (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_percentnumberoptionalGradual sends: share of the audience per batch, 1–50. Defaults to 10.
batch_intervalstringoptionalGradual sends: minute or hour between batches.
timezone_modestringoptionalspecific (default) sends at one instant; local sends at the same wall-clock time in each recipient’s own timezone.
send_timezonestringoptionalIANA 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_behaviorstringoptionalLocal-timezone sends, for recipients whose time already passed: next_day (default) or immediate.
recipients_at_send_timebooleanoptionalSnapshot the audience when the campaign sends instead of when it’s scheduled. Not available with timezone_mode: "local".
# 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"
}'{ "status": "sending", "recipients": 1240 }{
"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_enabledbooleanoptionalTurn A/B testing on. Enabling without variants seeds an identical A/B pair to edit.
ab_test_percentnumberoptionalShare 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_metricstringoptionalopen_rate (default, best for subject lines) or click_rate (best for content). Unique, human-only engagement.
ab_wait_minutesnumberoptionalHow long after the test batch finishes before the winner is decided, 30–10080. Default 360 (6 hours).
variantsarrayoptionalCreate 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).
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" }
]
}'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>" }
]
}'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.
{
"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 }
]
}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
/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 -X DELETE https://emailbump.com/api/v1/campaigns/CAMPAIGN_ID \
-H "Authorization: Bearer ebk_your_key"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.
{
"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."
}{
"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."
}