Campaigns
A campaign is an automated email sequence. It is made of steps (emails, delays, conditions) and sent to enrolled contacts.
Lifecycle
draft ──start──▶ active ──pause──▶ paused ──resume──▶ active ──▶ completed
└──────start──────┘| Action | Allowed from | Error otherwise (422) |
|---|---|---|
start | draft, paused | Campaign cannot be started from current status |
pause | active | Only active campaigns can be paused |
resume | paused | Only paused campaigns can be resumed |
update (PUT) | anything except active | Cannot update active campaign |
A campaign must have at least one step to start (422 Campaign must have at least one step).
start and resume run the same check before launch as the app. A blocking point refuses the action with a 422 that lists the blocking points: no email in the sequence, an email without subject or body, no enrolled contact who can start the sequence (launch only), no active sender that can send, no sending day or an empty sending window. Waits (sending window closed, daily limits reached) and points to check do not refuse it. Starting or resuming never enrolls contacts and never sends an email by itself: the sending engine takes the campaign afterwards, in its sending window.
{
"error": "HttpError",
"message": "Campaign cannot be started: No active sender with a daily limit above 0: nothing can be sent.",
"errors": { "launch": [ { "code": "no_sender", "object": "Senders", "message": "No active sender with a daily limit above 0: nothing can be sent." } ] },
"code": 422
}The campaign object
{
"id": 12,
"name": "Q2 outbound CTOs",
"status": "active",
"settings": {},
"sending_settings": {},
"stats": {
"total": 200, "pending": 120, "in_progress": 60,
"completed": 15, "failed": 2, "unsubscribed": 3
},
"started_at": "2026-04-01T08:00:00.000000Z",
"completed_at": null,
"created_at": "2026-03-28T10:00:00.000000Z",
"updated_at": "2026-04-01T08:00:00.000000Z",
"steps": [ /* Step */ ],
"contact_lists": [ /* List */ ],
"segments": [ /* Segment */ ]
}statsis included only when you add?include_stats=1.steps,contact_listsandsegmentsare included onGET /campaigns/{id}. Create, update and duplicate include the lists and segments.
Endpoints
| Method | Path | Permission |
|---|---|---|
| GET | /campaigns | campaigns:read |
| POST | /campaigns | campaigns:write |
| GET | /campaigns/{id} | campaigns:read |
| PUT | /campaigns/{id} | campaigns:write |
| POST | /campaigns/{id}/start | campaigns:send |
| POST | /campaigns/{id}/pause | campaigns:send |
| POST | /campaigns/{id}/resume | campaigns:send |
| POST | /campaigns/{id}/duplicate | campaigns:write |
| GET | /campaigns/{id}/stats | campaigns:read |
| GET | /campaigns/{id}/analytics | campaigns:read |
| GET | /campaigns/{id}/timeline | campaigns:read |
| GET | /campaigns/{id}/export | campaigns:read |
List campaigns
GET /campaigns?status=active&include_stats=1Returns campaigns newest first. The list is paginated.
Create a campaign
POST /campaigns| Field | Type | Rules |
|---|---|---|
name | string | Required. Max 255 characters |
settings | object | Free-form campaign settings |
list_ids | integer[] | Lists in your workspace to target |
segment_ids | integer[] | Segments in your workspace to target |
The campaign is created in draft.
Attaching lists or segments only sets the campaign's targets. It does not enroll anyone. To enroll contacts, call POST /campaigns/{id}/contacts/enroll-from-lists.
201: { "data": Campaign }
Update a campaign
PUT /campaigns/{id} accepts the same fields as create, and every field is optional.
list_ids and segment_ids replace the current targets. Send [] to clear them.
Typical flow
# 1. Create the campaign
curl -X POST https://app.beeving.com/api/v1/campaigns \
-H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"name": "Q2 outbound", "list_ids": [1]}'
# 2. Define the sequence
curl -X POST https://app.beeving.com/api/v1/campaigns/12/steps/bulk-update \
-H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"steps": [
{"type": "email", "settings": {"subject": "Hi {{first_name}}", "body": "<p>...</p>"}},
{"type": "delay", "settings": {"delay_value": 3, "delay_unit": "days"}},
{"type": "email", "settings": {"template_id": 7}}
]}'
# 3. Enroll the contacts
curl -X POST https://app.beeving.com/api/v1/campaigns/12/contacts/enroll-from-lists \
-H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"list_ids": [1]}'
# 4. Start
curl -X POST https://app.beeving.com/api/v1/campaigns/12/start \
-H "Authorization: Bearer TOKEN" -H "Accept: application/json"Duplicate
POST /campaigns/{id}/duplicateCreates a draft copy named "<name> (Copy)". The copy includes the settings, steps, lists and segments, but no enrolled contacts.
201: { "data": Campaign }
Stats
GET /campaigns/{id}/stats{
"data": {
"sent": 180, "delivered": 176, "delivery_confirmed": 0, "bounced": 4,
"opened": 92, "unique_opens": 92, "total_opens": 140,
"clicked": 25, "unique_clicks": 25, "total_clicks": 31,
"replied": 12, "positive_replies": 5, "unclassified_replies": 2, "unsubscribed": 2,
"delivery_rate": 97.8, "open_rate": 51.1, "click_rate": 13.9,
"bounce_rate": 2.2, "click_through_rate": 27.2,
"reply_rate": 6.7, "positive_reply_rate": 2.8, "unsubscribe_rate": 1.1,
"reply_categories": { "positive": 5, "out_of_office": 3, "unclassified": 2, "not_now": 2 }
}
}Rates are percentages of the emails sent, rounded to one decimal place, except click_through_rate (clicked / opened). The unit is the email: opened, clicked and replied count emails with at least one such interaction, total_opens and total_clicks count every tracking event. delivered means that no bounce was received; inbox placement is not measured. See metric definitions.
Analytics
GET /campaigns/{id}/analytics?days=14Returns the same stats, plus a day-by-day timeline that starts on the campaign's start date and covers days days, up to today. Each series counts emails: sent on that day, or opened, clicked or replied for the first time on that day:
{
"data": {
"stats": { /* as above */ },
"timeline": {
"categories": ["1 Apr", "2 Apr"],
"series": [
{ "name": "Emails sent", "data": [40, 38] },
{ "name": "Emails opened", "data": [12, 21] },
{ "name": "Emails clicked", "data": [2, 4] },
{ "name": "Emails replied", "data": [0, 1] }
]
}
}
}Timeline
GET /campaigns/{id}/timeline?per_page=50Returns the emails sent, newest first. The list is paginated.
{
"data": [{
"id": 901,
"contact": { "id": 42, "email": "alice@example.com", "full_name": "Alice Martin" },
"step": { "id": 3, "type": "email", "subject": "Hi Alice" },
"status": "delivered",
"sent_at": "...", "delivered_at": "...", "opened_at": "...", "clicked_at": null,
"open_count": 2, "click_count": 0
}],
"meta": { "current_page": 1, "last_page": 4, "per_page": 50, "total": 180 }
}The email status is one of queued, sending, sent, delivered, bounced, complained or failed.
Export
GET /campaigns/{id}/exportReturns a CSV file named campaign-{id}-results.csv, with one row per email sent.