> ## 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.

# Warm-up plan

> Give every number in a project a daily message allowance that ramps over its first weeks and moves with its health.

A new WhatsApp number that sends hundreds of messages on its first day is the
pattern WhatsApp restricts fastest. The warm-up plan gives every number in a
project a daily allowance that starts small, grows over the number's first
weeks, and shrinks when the number's health falls.

The warm-up plan is a BanSafe Lite feature, included on Standard and Pro. It is
off until you turn it on, and turning it off is always allowed.

## The allowance

Each number gets one allowance per day. It depends on two things: how long the
number has been active, and its current health band.

* On its first day a number may send **20** messages.
* The allowance grows in a straight line to the per-number daily cap of
  **2,000** over **14** days.
* The number's health band scales the result: **good** keeps all of it,
  **fair** three quarters, **poor** a half, **failing** a quarter. A number
  that has not been measured long enough to have a band keeps all of it, because
  the ramp is already the cautious part.
* The allowance never falls below the first-day figure and never rises above
  the cap.

A number seven days in, in good health, may send 1,010 messages that day. The
same number in poor health may send 505.

You can change the first-day figure and the length of the ramp. The 2,000
per-number daily cap is fixed, and campaigns already obey it.

## The day

The allowance resets at **midnight in the number's own country**, worked out
from its phone number. A Brazilian number rolls over at midnight in Brazil, a
British number at midnight in Britain. The read below reports the exact instant
of the next reset.

## How long a number has been active

The ramp counts from the earliest moment Polymorfa can prove the number was
active: when it was linked, or the oldest message in its history if that is
earlier. A number whose age cannot be proven starts the ramp on the day the
plan is turned on, and Console says so.

The current number Health response exposes that evidence as `tenureSource`: `history`
means an older history record anchors the ramp, `link` means a recorded pairing
does, and `plan` means neither was available so the project opt-in is the
anchor. Legacy evidence that cannot be classified reports `null` rather than
guessing a source.

## What happens at the allowance

Immediately before transport, Polymorfa durably reserves one slot in the
number's active allowance day. Concurrent sends and runner restarts use the same
counter, so they cannot spend the final slot more than once. A granted
reservation remains counted even if transport later fails or its result cannot
be confirmed. This keeps the configured allowance a strict ceiling.

A send past the day's allowance is refused before transport. Nothing on the
number changes and no restriction is applied; retry the unsent message after
the next reset. Replaying the same attempt within the same policy window
returns the same refusal. If Polymorfa
cannot durably authorize a slot, it pauses the send before transport and returns
`503 bansafe_accounting_unavailable` with `retryAfter: 5`. Retry the same
message after that interval without creating parallel retries.

If another attempt already received a slot for the same logical send, Polymorfa returns
`409 bansafe_send_outcome_unknown`. It does not send the message again. Do not
retry it automatically; reconcile its delivery state before deciding whether
to create a new message.

| Where              | What happens                                                                                                                                                                                                                                                   |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Messaging API send | Exhausted allowance returns `429 bansafe_daily_allowance_reached` with the seconds until reset. A temporary accounting pause returns `503 bansafe_accounting_unavailable` and `retryAfter: 5`. An unsafe duplicate returns `409 bansafe_send_outcome_unknown`. |
| Webhook            | `message.failed` carries the same stable code. Allowance and accounting refusals use `error: "blocked_by_safety"`; an unknown outcome uses `error: "send_failed"`. Retryable failures include `retryAfter`.                                                    |
| Campaigns          | Exhausted allowance defers the recipient to the next reset. Accounting unavailability requeues the unsent recipient after at least five seconds. An unknown send outcome is a terminal recipient failure and is not retried automatically.                     |

Creating a group and adding people to a group do not count against the
allowance. Safety limits still apply to both.

Messages sent by simulated test-mode sessions do not consume a daily allowance.

The allowance is a ceiling among ceilings. Your plan's throughput, a campaign's
own pacing, and any safety limit in force all still apply, and the smallest one
wins.

## Turn it on

The warm-up plan is in Console and on both APIs, with the same settings and the
same responses:

| Where         | Path                                          | Who uses it                                                          |
| ------------- | --------------------------------------------- | -------------------------------------------------------------------- |
| Console       | **Safety → Warm-up plan**                     | A signed-in team member                                              |
| Platform API  | `/platform/projects/{projectId}/warmup-plan`  | An organization API key, a project token, or a signed-in team member |
| Messaging API | `/messaging/projects/{projectId}/warmup-plan` | An organization API key or a project token                           |

Reading needs `sessions:read`; changing needs `sessions:manage`. On the Platform
API, a signed-in team member must be an organization owner, admin, or developer
to change the plan; reading needs organization membership.

```http theme={null}
GET  /platform/projects/{projectId}/warmup-plan
PUT  /platform/projects/{projectId}/warmup-plan
```

The body is a partial update: send only the settings you want to change.

```bash theme={null}
curl -X PUT "https://api.polymorfa.com/platform/projects/$PROJECT_ID/warmup-plan" \
  -H "Authorization: Bearer $POLYMORFA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true, "warmupDays": 14, "dailyStart": 20 }'
```

| Field        | Values            |
| ------------ | ----------------- |
| `enabled`    | `true` or `false` |
| `warmupDays` | 1 to 90           |
| `dailyStart` | 1 to 2000         |

The response carries the plan, the cap the ramp climbs to, and the allowance
day by day for a number in good health, so you can show the curve without
recreating it:

```json theme={null}
{
  "projectId": "3f9c...",
  "plan": { "enabled": true, "warmupDays": 14, "dailyStart": 20 },
  "ceiling": 2000,
  "curve": [
    { "day": 0, "allowance": 20 },
    { "day": 1, "allowance": 161 },
    { "day": 7, "allowance": 1010 },
    { "day": 14, "allowance": 2000 }
  ],
  "entitled": true,
  "entitlementReason": null
}
```

`entitled` is `true` when at least one number in the project is on a plan that
includes BanSafe Lite. When it is `false`, turning the warm-up plan on is
refused with `warmup_plan_not_entitled`, and `entitlementReason` carries the
sentence that refusal returns.

A write is stored before any number is told about it. If a connected number
does not confirm the new plan, the request answers `503 propagation_pending`
and the plan is already stored; retry the request and it is pushed again.

## See one number's allowance

`GET /platform/bansafe/health/{session}` carries a `warmup` block for the number:

```json theme={null}
{
  "warmup": {
    "enabled": true,
    "tenureSource": "history",
    "tenureDay": 7,
    "allowance": 1010,
    "sentToday": 640,
    "resetsAt": "2026-09-08T03:00:00.000Z",
    "curve": [{ "day": 0, "allowance": 20 }]
  }
}
```

`enabled` is `false` with a null `allowance` when the project has no warm-up
plan: there is no ceiling, rather than a ceiling of nothing. `curve` is drawn
at the number's fresh current Health band. Without a fresh supported
estimate, the band is unknown and the cautious tenure ramp applies without an
additional Health reduction.

`sentToday` comes from the durable active allowance window. A successful read
returns `0` before the first reservation of the day. Health history does not
contain allowance fields.

## Related

* [BanSafe overview](/guides/bansafe/overview)
* [Safety limits](/guides/bansafe/enforcement)
* [Safe Mode](/guides/bansafe/safe-mode)
