> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polymorfa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Campaigns and number safety

> What a campaign checks before it sends: the plan gate, the throughput ceilings, the opt-out list, per-recipient holds, and the warm-up ramp for a new number.

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](/guides/bansafe/findings) and
[Send a campaign](/guides/engage-at-scale/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](/api/errors#bansafe-and-safe-mode-codes).

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:

| When                    | What happens if no number in the campaign is on a plan with Campaigns                               |
| ----------------------- | --------------------------------------------------------------------------------------------------- |
| You create the campaign | `402 campaigns_not_entitled`                                                                        |
| You launch it           | `402 campaigns_not_entitled`                                                                        |
| While it is running     | The campaign pauses, and `campaign.paused` carries the reason and the code `campaigns_not_entitled` |

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:

| Field                | Default | Limit                                    |
| -------------------- | ------: | ---------------------------------------- |
| `ratePerMinute`      |      20 | At most 6,000, the Pro plan's throughput |
| `perNumberHourlyCap` |      60 | At most 300                              |
| `perNumberDailyCap`  |     500 | At most 2,000                            |
| `jitterMinMs`        |     800 | At least 800                             |
| `jitterMaxMs`        |   3,500 | Above `jitterMinMs`                      |
| `ackTimeoutMs`       |   5,000 | —                                        |
| `coldSendPolicy`     |  `hold` | `hold` or `allow`                        |

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](/guides/bansafe/enforcement#pacing-rates) 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](/guides/bansafe/safe-mode).

## Opted-out numbers are never messaged

A campaign checks your team's [opt-out list](/guides/engage-at-scale/opt-outs)
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](/guides/bansafe/enforcement) 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:

| What stopped the send                                                                                                                                  | The recipient                                                                                                      | The campaign                                                    |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- |
| The recipient is on the team's opt-out list                                                                                                            | Counted once in `skippedCount`, never attempted, with `lastError` `opted_out`                                      | Keeps running                                                   |
| The recipient has never written to this number and `coldSendPolicy` is `hold`                                                                          | Counted once in `skippedCount`, never attempted again, and reported by `campaign.cold_blocked`                     | Keeps running                                                   |
| The sending number is paced by a restriction                                                                                                           | Queued again and retried after the reported delay, with no delivery attempt spent and nothing counted as a failure | Keeps running, and `campaign.throttled` reports the pass        |
| The sending number or the whole workspace is suspended, the plan no longer includes Campaigns, or the requested throughput is above the safety ceiling | Left queued, with the attempt given back so no retry budget is spent                                               | Paused, with `campaign.paused` carrying the reason and the code |

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:

| Day | Daily cap |          Day | Daily cap |
| --: | --------: | -----------: | --------: |
|   0 |        20 |            7 |       260 |
|   1 |        54 |            8 |       294 |
|   2 |        88 |            9 |       328 |
|   3 |       122 |           10 |       362 |
|   4 |       157 |           11 |       397 |
|   5 |       191 |           12 |       431 |
|   6 |       225 |           13 |       465 |
|     |           | 14 and after |       500 |

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](/guides/bansafe/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.
