Skip to main content
Campaigns is the only bulk sending path in Polymorfa. A loop over POST /api/messages/send opens the bulk_outside_campaigns finding on the number that makes it, so outreach to people who have not written first belongs in a campaign, where the pacing, the per-number caps, the opt-out list, and the contact check are applied for you. See Findings and Send a campaign. This page states every check a campaign send passes and the exact refusal you get when one fails. Each code is listed in Errors. Both public surfaces run these checks. A campaign behaves the same whether you created and launched it through /messaging/projects/{projectSlug}/campaigns or through /platform/campaigns.

Campaigns is a Pro capability

Campaigns is included on Pro numbers. The check runs against the numbers the campaign can actually send from: the numbers it pins with senderConfig.sessionIds, or every number in its project when it pins none. When the project has no numbers yet, the project’s default plan is used, because that is the plan the next number linked to it inherits. The check runs three times: The launch check is not the create check remembered. A campaign drafted while the numbers were on Pro is refused at launch if they moved to another plan in between, and a running campaign whose numbers all lose Campaigns pauses instead of continuing against a pool that can no longer send. Move the numbers back to Pro and resume. A number whose plan cannot be resolved is treated as not entitled. The gate fails closed.

Throughput limits

senderConfig.throughput sets the campaign’s pacing. A value above its limit is refused when you send it, at create and at launch, with 400 campaign_throughput_capped naming the field: A campaign that is already running is brought down to the limit rather than stopped, so a change mid-flight slows the campaign instead of breaking it. The caps are per number, not per campaign: two campaigns sharing a number share its caps. While a number is paced by a restriction, the campaign sends at the lower of its own rate and the paced rate, which Restrictions lists.

Every campaign send is typed and paced

A send that comes from a campaign always uses at least a typing indicator before each text message and jittered pacing, whatever Safe Mode settings the number carries. A campaign never raises the number’s presence or read-receipt settings, because those are visible to your contacts in ways typing and pacing are not. The pause a send takes is bounded by the campaign’s own gap between recipients, so pacing never reduces the throughput you configured. See Safe Mode.

Opted-out numbers are never messaged

A campaign checks your team’s opt-out list twice. When the campaign launches, every recipient on the list is marked skipped with lastError opted_out and counted once in skippedCount. At send time, a number on the list is never claimed, so a number added to the list after the campaign launched is still suppressed, and re-queuing a skipped recipient does not send to it.

Recipients who never wrote first are held

The contact check is made per recipient, at send time, on the number that is about to send. A recipient who has never written to that number is held rather than messaged, and the hold carries 409 bansafe_cold_held. Two things change the outcome:
  • Set senderConfig.throughput.coldSendPolicy to allow to message people who have not written first. This raises the number’s ban risk; the default is hold.
  • Send to people who wrote to that number. The check is per number, so a contact who wrote to one of your numbers is still cold on another.
The hold is not a failure of the number. It is a fact about the recipient list. It applies today, on every plan that includes Campaigns, and it does not depend on the restriction ladder: it is your campaign’s own coldSendPolicy. See Restrictions for the ladder itself and how each step lifts.

What safety does to a recipient

A campaign send passes the number’s restriction step as well as the checks above. What happens to the recipient depends on which check stopped it: A recipient safety holds back is never a delivery failure. failedCount counts sends WhatsApp refused, not sends Polymorfa never made.

Stopping and deleting

Stop always means cancel. It moves a draft, running or paused campaign to cancelling, then to cancelled once sends already in progress finish, and the recipients not yet sent are not sent. Stop is never blocked by a workspace suspension or by a lost plan entitlement, so a campaign a check paused can always be stopped. Delete removes a draft, completed, failed, cancelled or archived campaign. A campaign that is still live, including a draft with a scheduled launch, is refused with 409 state_conflict: stop it first, then delete it. If a completed or failed campaign still has a final event to deliver, delete or archive returns 409 state_conflict. Retry after that event is delivered.

A new number ramps up

A number’s daily cap starts low and rises to the campaign’s perNumberDailyCap over warmupDays days, in a straight line. With the defaults — warmupDailyStart 20, perNumberDailyCap 500, warmupDays 14 — the ceiling on a day is: A number is treated as new for its first 14 days here and in the warmup_ignored finding, which starts at the same 20 messages a day. The ramp alone does not keep that finding closed: check it on the number while a new campaign runs. See Findings. The ramp is anchored to how old the number is, not to when it first ran a campaign: Polymorfa uses the earliest instant it can prove the number was in use, such as when the number was linked or the oldest message its history carries. The anchor only ever moves earlier, so a number that is already warm cannot be pushed back onto the ramp. A number whose age cannot be established is treated as warm and sends at the full cap. While a number is on the ramp, recipients beyond the day’s cap are not dropped. They stay queued for the next window, and campaign.cap_reached reports the number, the cap, and when the window resets.