Skip to main content

Messaging Campaigns

Campaigns let you send bulk messages to contact lists across WhatsApp, Telegram, Messenger, Instagram, and SMS. This guide covers creating, scheduling, sending, and analyzing campaigns.

Required Scopes

ScopePurpose
campaigns:readList and view campaigns and analytics, check warm-up status, preview messages per recipient
contacts:readNeeded in addition to campaigns:read for the previews that show contact details: POST /campaigns/preview/recipients, POST /campaigns/preview and POST /campaigns/preview-segment
campaigns:createCreate new campaigns
campaigns:updateEdit campaign content/settings and schedule/cancel-schedule
campaigns:sendSend, pause, resume, and cancel campaigns
campaigns:deleteDelete draft campaigns

Campaign Lifecycle

StatusDescription
draftCampaign is being prepared, not yet sent
scheduledCampaign is scheduled for future delivery
sendingCampaign is actively being sent
throttledSending is on hold temporarily and continues automatically (see throttle_reason, e.g. daily_batch_limit between the days of a split send)
sentCampaign has finished sending
pausedCampaign was paused during sending
cancelledCampaign was cancelled before completion

Campaign Pricing

Campaign messages are billed at the same unified per-unit rate as conversation messages and KB queries, and draw from the same monthly included pool that comes with your plan's base fee:

PlanBase feeIncluded pool / monthPer campaign message beyond the pool
BasicEUR 492,500EUR 0.020
ProfessionalEUR 792,500EUR 0.020
ScaleEUR 19910,000EUR 0.015
EnterpriseEUR 49940,000EUR 0.010
API OnlyEUR 9 per connected channel1,000 per connected channelEUR 0.005

Incoming messages are not charged. Campaign emails have their own (lower) rate — see Email Campaigns. Carrier/network fees for SMS are passed through on top of these rates.

Create a Campaign

POST /api/v1/campaigns

Campaign content is per-channel, supplied via the channel_content object. Include only the channels the campaign targets. Each channel has its own content shape — text channels (Telegram, SMS, Messenger) take a message_text, WhatsApp requires a pre-approved template, and browser push takes a title/body.

Request Body

FieldTypeRequiredDescription
namestringYesCampaign name
list_idsarrayYesArray of contact list IDs to send to
channel_contentobjectYes*Per-channel content (see shapes below). *Legacy single-channel campaigns may instead use top-level message_text + message_type.
descriptionstringNoInternal description
scheduled_atstringNoISO 8601 datetime. Can also be set later via the dedicated schedule endpoint.
timezonestringNoIANA timezone (e.g. Europe/Berlin) for scheduled_at
should_skip_duplicatesbooleanNoDefault true. Skip contacts already messaged within the duplicate window.
duplicate_window_hoursintegerNoDefault 24. Window for duplicate skipping.
send_split_configobjectNoWhatsApp only. Spread the send over several days — see Split sending. Omit or null to send to everyone at once.
acknowledge_warmup_warningbooleanNoDefault false. Only used together with scheduled_at: confirms a large WhatsApp send while the number is still warming up (see Warm-up acknowledgement).

A campaign created with scheduled_at is scheduled right away, so it goes through the same checks as the schedule endpoint: 409 warmup_acknowledgement_required if it needs a warm-up acknowledgement and acknowledge_warmup_warning is not true, and 422 split_plan_too_long if its split plan would run too long. In both cases no campaign is created, so you can safely repeat the request.

Every ID in list_ids must be a list in your workspace. If one is not, the request is rejected with 400 "One or more lists not found". This applies both when you create a campaign and when you change its list_ids with PUT /api/v1/campaigns/{campaign_id}, and a rejected update leaves the campaign unchanged.

channel_content shapes

ChannelKeyFields
Telegramtelegrammessage_text (required), message_type (text/image/video/document), attachment_id, channel_id
SMSsmsmessage_text (required), channel_id
Messengermessengermessage_text (required), message_type, attachment_id, channel_id
WhatsAppwhatsappchannel_ids (required), template_id (required), template_name (required), template_language, variable_mapping
Browser Pushbrowser_pushtitle (required), text (required), url, image_attachment_id, channel_id
WhatsApp requires templates

Business-initiated WhatsApp campaigns can only send approved message templates — you cannot send free-form text. See WhatsApp Templates for how to create and reference a template, then pass its template_id/template_name in channel_content.whatsapp.

Create a Draft (Telegram)

curl -X POST "https://api.sendseven.com/api/v1/campaigns" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"name": "Weekend Sale Reminder",
"list_ids": ["list_active_customers", "list_newsletter"],
"channel_content": {
"telegram": {
"message_type": "text",
"message_text": "Our weekend sale starts tomorrow! 30% off all items."
}
}
}'

Response (201 Created)

The created campaign starts in draft status. Recipient and delivery counters are returned as discrete fields:

{
"id": "c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8",
"name": "Weekend Sale Reminder",
"status": "draft",
"channel_content": {
"telegram": { "message_type": "text", "message_text": "Our weekend sale..." }
},
"list_ids": ["list_active_customers", "list_newsletter"],
"total_recipients": 3800,
"sent_count": 0,
"delivered_count": 0,
"read_count": 0,
"failed_count": 0,
"scheduled_at": null,
"timezone": null,
"created_at": "2026-02-10T16:30:00Z",
"updated_at": "2026-02-10T16:30:00Z"
}

Preview a Campaign per Recipient

Before you save or send, you can see exactly what each recipient will get: the personalization variables, template header, body and buttons are filled in the same way as for the real send. These endpoints work without a saved campaign, so you can use them while you build one. They also work for an existing campaign with unsaved changes. They are read-only: nothing is saved, nothing is sent, and nothing is billed.

EndpointRequired scopes
POST /campaigns/audience/channelscampaigns:read
POST /campaigns/preview/recipientscampaigns:read and contacts:read
POST /campaigns/previewcampaigns:read and contacts:read
POST /campaigns/preview/variable-statscampaigns:read
POST /campaigns/preview-segmentcampaigns:read and contacts:read

/preview/recipients, /preview and /preview-segment show contact details (names, phone numbers, email addresses and custom fields), so they also need contacts:read. A token that has only one of the two scopes gets 403.

The request bodies of /audience/channels, /preview/recipients and /preview/variable-stats accept ignores_newsletter_subscriptions only for compatibility. The only accepted value is false, which is also the default. true ("send to all contacts") is no longer supported and returns 422 with detail.code ignores_newsletter_subscriptions_not_supported. Creating and updating campaigns rejects it too.

Channels that reach your lists

POST /api/v1/campaigns/audience/channels
{ "list_ids": ["d1e2f3a4-5b6c-4d7e-8f9a-0b1c2d3e4f5a"] }
{
"items": [
{ "channel_id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e", "channel_type": "whatsapp", "channel_name": "Shop WhatsApp", "subscriber_count": 3800 },
{ "channel_id": "a7b8c9d0-1e2f-4a3b-8c4d-5e6f7a8b9c0d", "channel_type": "telegram", "channel_name": "Shop Bot", "subscriber_count": 412 }
]
}

Only active channels with at least one reachable subscriber in the selected lists are returned. Use these channels for the campaign's channel_content.

Find recipients to preview

POST /api/v1/campaigns/preview/recipients
{
"list_ids": ["d1e2f3a4-5b6c-4d7e-8f9a-0b1c2d3e4f5a"],
"channel_id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"search": "müller",
"page": 1,
"page_size": 10
}
{
"items": [
{ "contact_id": "e5f6a7b8-9c0d-4e1f-a2b3-c4d5e6f7a8b9", "display_name": "Anna Müller", "phone": "+491701234567", "email": "[email protected]", "channel_id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e" }
],
"pagination": { "total": 1, "page": 1, "page_size": 10, "total_pages": 1, "has_next": false, "has_prev": false }
}

These are the same recipients the send would reach: subscribers of the lists, without opt-outs, routed to the channel. channel_id and search are optional. search matches name, phone or email, and case does not matter. page_size is at most 50.

HubSpot segment lists

/preview, /preview/recipients and /preview/variable-stats never call HubSpot. For a HubSpot segment list, /preview/recipients only shows contacts that are already stored in your workspace:

  • Cached segments (refreshed on a schedule) show the members from the last refresh. The send uses the same members.
  • Segments resolved at send time show the contacts that the most recent campaign sent to that list reached. A list that has never been sent to shows no recipients, and HubSpot members that the send would add as new contacts are never shown. The real send still fetches the current members from HubSpot, so its audience can be larger or different.

/audience/channels is different: it counts HubSpot segment lists the same way the send does, including current HubSpot members, so channels are not hidden because a list has not been sent to yet. Counting never creates contacts.

Render the message for one recipient

POST /api/v1/campaigns/preview
{
"contact_id": "e5f6a7b8-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"channel_id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"list_ids": ["d1e2f3a4-5b6c-4d7e-8f9a-0b1c2d3e4f5a"],
"draft": {
"channel_content": {
"whatsapp": {
"channel_ids": ["b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e"],
"template_id": "f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d",
"template_name": "autumn_sale",
"template_language": "de",
"variable_mapping": { "1": "contact.first_name" }
}
}
}
}
  • draft takes the same content fields as updating a campaign: channel_content, message_text, message_type, attachment_id, the whatsapp_template_* fields, and so on.
  • Add campaign_id to preview an existing campaign. Only the fields you send in draft replace its saved values, and the saved campaign itself is not changed.
  • Without campaign_id, only the draft is rendered.
  • The contact does not need to be on the lists, but the contact and the channel must belong to your workspace.
{
"channel_id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"channel_type": "whatsapp",
"message_type": "whatsapp_template",
"text": null,
"media": null,
"template": {
"name": "autumn_sale",
"language": "de",
"header": { "format": "IMAGE", "text": null, "media_url": "https://cdn.example.com/autumn.jpg" },
"body": "Hallo Anna, unser Herbst-Sale startet heute!",
"footer": "Abmelden: STOP",
"buttons": [ { "type": "URL", "text": "Zum Shop", "url": "https://shop.example.com/sale", "phone_number": null } ],
"cards": null
},
"variables": [
{ "placeholder": "{{1}}", "source": "contact.first_name", "value": "Anna", "status": "resolved" }
],
"has_unresolved": false,
"recipient": { "contact_id": "e5f6a7b8-9c0d-4e1f-a2b3-c4d5e6f7a8b9", "display_name": "Anna Müller", "address": "+491701234567" }
}

For a normal (non-template) message, text holds the final message text and media the attachment (type, url, filename); template is then null. recipient.address is the phone number or handle the message would go to on this channel.

Each entry in variables has a status:

statusMeaning
resolvedThe recipient has a value for this field.
emptyThe field exists but is empty for this recipient.
missing_fieldThe field does not exist for this recipient.
fallbackThe fallback value was used.

has_unresolved is true if any variable is not resolved.

Missing values across the audience

POST /api/v1/campaigns/preview/variable-stats
{
"channel_id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"list_ids": ["d1e2f3a4-5b6c-4d7e-8f9a-0b1c2d3e4f5a"],
"draft": { "channel_content": { "whatsapp": { "variable_mapping": { "1": "contact.first_name" } } } }
}
{
"scanned": 3800,
"total": 3800,
"capped": false,
"variables": [
{ "placeholder": "{{1}}", "source": "contact.first_name", "missing_count": 112, "empty_count": 9, "fallback_count": 0 }
],
"counts_exclude_hubspot": false,
"excluded_hubspot_segment_list_ids": []
}

This counts, per variable, how many recipients on the channel would get an empty, missing or fallback value. The same campaign_id and draft rules apply as for POST /preview. For large audiences only part of the audience is checked: capped is then true, and scanned shows how many recipients were checked out of total.

HubSpot segment lists are left out of these counts:

FieldMeaning
counts_exclude_hubspottrue when at least one selected list is a HubSpot segment and was left out of the counts
excluded_hubspot_segment_list_idsIDs of the HubSpot segment lists that were left out

If every selected list is a HubSpot segment, the response has scanned: 0, total: 0 and an empty variables array.

Preview a segment

POST /api/v1/campaigns/preview-segment

Returns total_count (all contacts that match the segment filters) and preview_contacts, a sample with id, name, phone_number and email. limit sets the sample size. It defaults to 10 and can be at most 100; a larger value returns 422. Requires campaigns:read and contacts:read (403 otherwise).

Schedule a Campaign

Scheduling uses a dedicated endpoint that accepts a local datetime plus an IANA timezone (the backend converts to UTC for storage). The scheduled time must be at least 5 minutes in the future. Requires the campaigns:update scope.

POST /api/v1/campaigns/{campaign_id}/schedule
curl -X POST "https://api.sendseven.com/api/v1/campaigns/c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8/schedule" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"scheduled_at": "2026-02-15T08:00:00",
"timezone": "Europe/Berlin"
}'

The campaign moves to scheduled status. To unschedule, call POST /api/v1/campaigns/{campaign_id}/cancel-schedule.

The body also accepts acknowledge_warmup_warning (boolean, default false). The same warm-up rule as for sending applies when you schedule: if the campaign needs an acknowledgement you get 409 warmup_acknowledgement_required until you pass true. Confirm the warning when you schedule, not later: the acknowledgement is stored with the campaign, and the scheduled send goes out on time without asking again. The only exception is a significant audience growth between scheduling and the send time.

If you change the audience or content of a scheduled campaign afterwards (list_ids, channel_content, channel_filter, selected_channel_ids, send_split_config or any WhatsApp template field), the stored acknowledgement is removed, because it applied to the old version. Renaming or rescheduling keeps it. When the campaign is due and still needs an acknowledgement (none was given, it was removed by an edit, or the audience grew significantly since), it is not sent: it moves to paused with throttle_reason warmup_acknowledgement_required, and warmup_assessment.hold.kind on the campaign tells you why (not_acknowledged or audience_grew). Resume it with {"acknowledge_warmup_warning": true} (see Resume after a warm-up hold), or schedule it again with the acknowledgement.

A split plan that would run too long for the campaign's audience is rejected with 422 and detail.code split_plan_too_long (see Split sending).

Send a Campaign

Immediately begin sending a draft campaign. Requires the campaigns:send scope.

POST /api/v1/campaigns/{campaign_id}/send
curl -X POST "https://api.sendseven.com/api/v1/campaigns/c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"

The body is optional. Send {"acknowledge_warmup_warning": true} to confirm a large WhatsApp send while the number is still warming up (see Warm-up acknowledgement). Sending with no body is the same as sending false.

{
"success": true,
"campaign_id": "c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8",
"recipient_count": 3800,
"published_count": 3800,
"split": {
"days_total": 1,
"next_batch_at": null,
"scheduled_count": 0
},
"message": "Campaign queued for sending to 3800 recipients (3800 messages published)"
}

published_count is the number of messages queued now. For a split send, split.days_total is the number of sending days, split.scheduled_count the number of messages held for later days, and split.next_batch_at (UTC, ISO 8601) the start of the next daily batch. Without split sending, days_total is 1 and nothing is scheduled.

A sending campaign can be paused, resumed, or cancelled (all require campaigns:send):

POST /api/v1/campaigns/{campaign_id}/pause
POST /api/v1/campaigns/{campaign_id}/resume
POST /api/v1/campaigns/{campaign_id}/cancel

You can also pause a split send between two days, while it is throttled with throttle_reason daily_batch_limit. A paused campaign stays paused until you resume it. On resume, the remaining recipients are planned again from that moment: today's batch goes out, and the next batches follow on the next days, so a long pause does not end in one large burst.

Resume after a warm-up hold

POST /resume accepts an optional body {"acknowledge_warmup_warning": true}; no body is the same as false. The flag matters in two cases:

  • The campaign is paused with throttle_reason warmup_acknowledgement_required. This happens when a scheduled campaign was due but still needed a warm-up acknowledgement, for example because it was edited after it was scheduled.
  • The campaign is resumed before any of its messages were prepared, for example because it failed at the very start.

In both cases resume runs the same warm-up check as sending. Without the acknowledgement it returns 409 warmup_acknowledgement_required and the campaign stays paused. With it, the acknowledgement is recorded and the campaign is sent like a new send: messages are prepared, and with split sending only day 1 goes out now. An ordinary pause needs no acknowledgement.

A campaign held because its audience grew (warmup_assessment.hold.kind audience_grew) always needs {"acknowledge_warmup_warning": true} to resume, even if an earlier acknowledgement is stored. A successful resume removes warmup_assessment.hold.

Retry Failed Recipients

Recipients that ended in failed are not re-sent by resume. Use retry-failed to reset them to pending and queue them again. The typical case is an SMS campaign that was paused automatically because the prepaid SMS balance was used up: top up, then retry only the recipients that failed for that reason. Requires the campaigns:send scope and a campaign in status completed, failed or paused.

POST /api/v1/campaigns/{campaign_id}/retry-failed

The body is optional. error_codes restricts the retry to recipients whose recorded error code matches; omit it to retry every failed recipient.

curl -X POST "https://api.sendseven.com/api/v1/campaigns/c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8/retry-failed" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{"error_codes": ["insufficient_sms_balance"]}'
{
"success": true,
"id": "c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8",
"requeued": 412
}

requeued is the number of failed recipients reset to pending. The campaign returns to sending; recipients that were still pending (for example the unsent tail of a paused campaign) are queued together with them, so a single call resumes the whole campaign. A 400 is returned for campaigns in any other status.

For a split send, the retried recipients join the remaining plan, which is planned again from that moment, just like resume: only today's batch goes out now, and the rest follows on the next days.

Error codes recorded on failed SMS recipients:

error_codeMeaningRetry after
insufficient_sms_balancePrepaid SMS balance used up; the campaign is paused automaticallyTopping up SMS credit
sms_not_provisionedThe workspace has no SMS subaccount yetCompleting the SMS channel setup
sms_duplicate_sendIdentical message to this recipient within the last 180 seconds3 minutes
sms_daily_limit_reachedDaily message limit for this recipient reachedThe next day

WhatsApp Warm-up and Split Sending

Meta limits how many people a WhatsApp number can message per day, and it watches how recipients react to a new number. A large first send from a new or little-used number can lower its quality rating or get it restricted. SendSeven helps in two ways:

  • Warm-up check – before a WhatsApp campaign goes out, SendSeven checks whether the sending number is still warming up and whether the audience is large. If so, you must confirm the send explicitly.
  • Split sending – send the campaign in daily batches (for example 250 on day 1, 500 on day 2, and so on) instead of all at once.

Both apply to WhatsApp channels only. See WhatsApp newsletter best practices for background.

Check warm-up status

GET /api/v1/campaigns/{campaign_id}/warmup-check

Requires campaigns:read. Works for drafts, before any message has been prepared. It uses the campaign's saved send_split_config.

To see how a different split would change the result without saving it, post the config instead:

POST /api/v1/campaigns/{campaign_id}/warmup-check
{ "send_split_config": { "mode": "ramp", "daily_batches": [250, 500, 1000], "batch_size": null, "send_time_local": "09:00" } }

An invalid config returns 422 with detail.code invalid_split_config. null checks the saved config, the same as the GET.

Response:

{
"applies": true,
"warmup": true,
"requires_acknowledgement": true,
"audience_size": 4200,
"recommended_daily_batches": [250, 500, 1000, 2000, 450],
"recommended_days": 5,
"guide_url": "https://docs.sendseven.com/guides/campaigns/whatsapp-newsletter-best-practices",
"acknowledged_at": null,
"acknowledged_by_user_id": null,
"split_config": null,
"thresholds": {
"large_audience": 250,
"channel_age_days": 30,
"min_completed_campaigns": 3,
"min_total_sent": 1000,
"ramp_first_batch": 250
},
"channels": [
{
"channel_id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
"channel_name": "Shop WhatsApp",
"audience_size": 4200,
"warmup": true,
"requires_acknowledgement": true,
"reasons": [
{ "code": "new_channel", "params": { "channel_age_days": 3 } },
{ "code": "audience_exceeds_tier", "params": { "tier_cap": 1000 } }
],
"meta_tier": "TIER_1K",
"tier_daily_cap": 1000,
"quality_rating": "GREEN",
"meta_limit_available": true,
"channel_age_days": 3,
"completed_campaigns": 0,
"total_sent": 0,
"recommended_daily_batches": [250, 500, 1000, 1000, 1000, 450]
}
]
}
FieldDescription
appliesfalse when the campaign has no WhatsApp channel. The other fields are then neutral.
warmupAt least one WhatsApp number is still warming up.
requires_acknowledgementSending or scheduling needs acknowledge_warmup_warning: true.
audience_sizeWhatsApp recipients across all numbers of the campaign.
recommended_daily_batches / recommended_daysSuggested daily ramp for the most limited number. You can pass it straight into send_split_config as {"mode": "ramp", "daily_batches": [...]}.
thresholdsThe limits the check currently uses (the values above are examples).
channels[]Result per WhatsApp number, with its reasons, Meta messaging tier (meta_tier, tier_daily_cap, null when unknown) and quality rating.
assessment_errorOnly present (true) when the check could not be completed. The campaign is then treated as needing an acknowledgement.

Reason codes in channels[].reasons[].code:

CodeMeaning
new_channelThe number was connected recently (params.channel_age_days).
few_previous_campaignsFew campaigns have been completed on this number so far.
low_send_historyFew campaign messages have been delivered through this number so far.
audience_exceeds_tierThe audience is larger than the number's daily Meta messaging limit (params.tier_cap).
quality_not_highThe number's Meta quality rating is not high.
meta_limit_unavailableInformation only: the Meta tier/quality could not be read. The history checks still apply.

Warm-up acknowledgement

An acknowledgement is needed when a number is warming up and its audience is large, unless split sending already keeps that number's first day small. In that case POST /send, POST /schedule, POST /campaigns with scheduled_at, and POST /resume after a warm-up hold return 409 and nothing is sent:

{
"detail": {
"code": "warmup_acknowledgement_required",
"message": "This WhatsApp number is still warming up and the campaign is large. ...",
"warmup": { "applies": true, "requires_acknowledgement": true, "...": "same shape as the warmup-check response" }
}
}

You then have two options:

  1. Split the send – save a send_split_config (for example the recommended ramp) with PUT /api/v1/campaigns/{campaign_id} and send again.
  2. Send anyway – repeat the call with {"acknowledge_warmup_warning": true}.

The acknowledgement is recorded on the campaign in warmup_acknowledged_at (UTC) and warmup_acknowledged_by_user_id (the user behind the token), together with the audience size you confirmed (warmup_assessment.acknowledgement). The warning is a safeguard, not a block: with the acknowledgement the campaign is sent as requested.

A stored acknowledgement keeps counting: a later POST /send, POST /schedule or POST /resume of the same campaign, and the scheduled send itself, do not ask again. It is removed when you change the campaign's audience or content (see Schedule a Campaign).

Audience growth after the acknowledgement

Your lists can grow between the acknowledgement and the send. If the WhatsApp audience grew by more than 20% and by at least 250 recipients since you confirmed, and the campaign still needs an acknowledgement, the stored acknowledgement no longer counts:

  • POST /send, POST /schedule and POST /resume without acknowledge_warmup_warning: true return 409. detail.warmup.reasons then contains audience_grew_since_acknowledgement, and detail.warmup.acknowledgement_growth shows the numbers:
{
"detail": {
"code": "warmup_acknowledgement_required",
"message": "This WhatsApp number is still warming up and the campaign is large. ...",
"warmup": {
"requires_acknowledgement": true,
"reasons": ["...", "audience_grew_since_acknowledgement"],
"acknowledgement_growth": {
"acknowledged_audience_size": 1000,
"current_audience_size": 1600,
"growth_pct": 60.0,
"acknowledged_at": "2026-09-20T09:00:00Z"
},
"...": "same shape as the warmup-check response"
}
}
}
  • A scheduled campaign is not sent at its send time: it is held paused with warmup_assessment.hold.kind audience_grew (see Resume after a warm-up hold).

Confirm again with {"acknowledge_warmup_warning": true}; this stores the new audience size. Smaller growth never triggers a new warning.

Split sending

Set send_split_config when you create or update a campaign (PUT /api/v1/campaigns/{campaign_id}). It can be changed while the campaign is draft, approved or scheduled. Send "send_split_config": null to remove it again; leaving the field out keeps the saved value. An approved campaign accepts an update that only changes send_split_config.

{ "mode": "ramp", "daily_batches": [250, 1000, 5000], "batch_size": null, "send_time_local": "09:00" }
{ "mode": "fixed", "daily_batches": null, "batch_size": 500, "send_time_local": null }
FieldDescription
moderamp (growing daily batches) or fixed (the same size every day).
daily_batchesramp only. Recipients per day, day 1 first. The last value repeats until everyone is reached.
batch_sizefixed only. Recipients per day (at least 1).
send_time_localOptional HH:MM. Days 2 and later start at this time in the campaign's timezone (UTC if none is set). If omitted, each day starts at the same time of day as the first batch.

Batches are counted per WhatsApp number, because Meta's limits apply per number. Recipients on other channels of the same campaign all get the message on day 1. A plan may cover at most 60 days; a longer plan is rejected with invalid_split_config.

The actual number of days depends on how many recipients each number reaches, so it is checked again against the campaign's real audience when you send it, schedule it (including POST /campaigns with scheduled_at), or resume one that was never prepared. If the plan would run too long, the request returns 422 with detail = {"code": "split_plan_too_long", "message": ...} and nothing is sent or scheduled. Use larger daily batches or split the audience across campaigns.

Validation errors return 422 with detail = {"code": ..., "message": ...}:

detail.codeMeaning
invalid_split_configThe config shape or values are invalid (see message).
split_sending_requires_whatsappThe campaign has no WhatsApp channel.
split_config_not_editableThe campaign is already sending or finished.

While a split send runs, the campaign shows its progress in send_split_state:

{
"plan": [250, 1000, 5000, 5000],
"days_total": 4,
"current_day": 1,
"next_batch_at": "2026-09-24T07:00:00+00:00",
"day_started_at": "2026-09-23T07:00:01+00:00",
"timezone": "Europe/Berlin"
}

When a day's batch is done, the campaign waits in status throttled with throttle_reason daily_batch_limit and continues on its own at the next batch time. The campaign is completed once every day's batch has been sent. You can pause and resume it between days.

Get Campaign Statistics

Retrieve delivery metrics for a campaign. Requires the campaigns:read scope.

GET /api/v1/campaigns/{campaign_id}/statistics
curl -X GET "https://api.sendseven.com/api/v1/campaigns/c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8/statistics" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json"
{
"campaign_id": "c3d4e5f6-7a8b-49c0-d1e2-f3a4b5c6d7e8",
"campaign_name": "Weekend Sale Reminder",
"status": "sent",
"total_recipients": 2450,
"sent_count": 2450,
"delivered_count": 2398,
"read_count": 1120,
"failed_count": 52,
"pending_count": 0,
"skipped_count": 0,
"status_counts": { "sent": 2450, "delivered": 2398, "read": 1120, "failed": 52 },
"started_at": "2026-02-15T08:00:05Z",
"completed_at": "2026-02-15T08:14:32Z"
}
Messaging vs. email metrics

Messaging campaigns track delivery stats (sent/delivered/read/failed). Open-rate, click-rate, and bounce metrics are specific to Email Campaigns, which expose them via GET /api/v1/email-campaigns/{id}/analytics.

List Campaigns

GET /api/v1/campaigns

Returns a plain JSON array of campaigns (not a paginated items/pagination envelope). Paginate with limit (default 50) and offset, and optionally filter by campaign_status.

curl -X GET "https://api.sendseven.com/api/v1/campaigns?limit=20&offset=0&campaign_status=sent" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00"

Python Example

import requests

BASE_URL = "https://api.sendseven.com/api/v1"
HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
}

# Create a draft campaign (Telegram)
campaign = requests.post(
f"{BASE_URL}/campaigns",
headers=HEADERS,
json={
"name": "Flash Sale Alert",
"list_ids": ["list_active_customers"],
"channel_content": {
"telegram": {
"message_type": "text",
"message_text": "24-hour flash sale! 50% off everything. Shop now: shop.example.com",
}
},
},
).json()
print(f"Created: {campaign['id']} ({campaign['total_recipients']} recipients)")

# Send it immediately
result = requests.post(f"{BASE_URL}/campaigns/{campaign['id']}/send", headers=HEADERS).json()
print(result["message"])

# Check statistics later
stats = requests.get(
f"{BASE_URL}/campaigns/{campaign['id']}/statistics",
headers=HEADERS,
).json()
print(f"Delivered: {stats['delivered_count']}/{stats['sent_count']}, read: {stats['read_count']}")

JavaScript Example

const BASE_URL = "https://api.sendseven.com/api/v1";
const HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
};

// Create a draft campaign
const createRes = await fetch(`${BASE_URL}/campaigns`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
name: "Valentine's Day Special",
list_ids: ["list_newsletter"],
channel_content: {
telegram: {
message_type: "text",
message_text: "Show someone you care with our Valentine's collection. Order by Feb 12!",
},
},
}),
});
const campaign = await createRes.json();

// Schedule it for a future time (local datetime + IANA timezone)
await fetch(`${BASE_URL}/campaigns/${campaign.id}/schedule`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ scheduled_at: "2026-02-12T08:00:00", timezone: "Europe/Berlin" }),
});
console.log(`Campaign ${campaign.id} scheduled.`);

Error Responses

StatusError CodeDescription
401INVALID_TOKENToken is invalid or expired
403INSUFFICIENT_SCOPEToken lacks required campaigns scope
404RESOURCE_NOT_FOUNDCampaign not found
404list_not_found / channel_not_found / contact_not_foundA list, channel or contact in a preview request does not belong to your workspace (details)
409CONFLICTCannot modify a campaign that is already sending or sent
409warmup_acknowledgement_requiredLarge WhatsApp send during number warm-up; retry with acknowledge_warmup_warning: true or use split sending (details)
422VALIDATION_ERRORInvalid request (e.g., empty list_ids)
422invalid_split_config / split_sending_requires_whatsapp / split_config_not_editablesend_split_config was rejected (details)
422split_plan_too_longThe split plan would take too many days for the campaign's audience (details)

Next Steps