Skip to main content
A campaign turns a list of numbers into paced, safety-checked WhatsApp messages sent from the numbers in one project. This guide runs the whole path through the API: build the recipient list, launch, follow the send, and stop. Campaigns are the only bulk sending path in Polymorfa. Every send passes the plan, throughput, opt-out, contact and warm-up checks described in Campaigns and number safety.

Pick a surface

Two public paths reach the same campaign engine with the same checks, the same errors and the same results. Reading needs campaigns:read. Creating, launching, rescheduling, pausing, resuming, stopping and deleting need campaigns:manage. The examples below use the Messaging API; the Platform API takes the campaign id in the path. On the Platform API a team API key names the project with projectId on every campaign request, and a project token is bound to its own project and omits it.

Create the campaign with its recipients

Use the console

In the campaign builder, choose an audience, import a CSV, or paste phone numbers. The import summary shows the recipients kept, duplicates, and invalid rows. For pasted numbers, the summary stays visible after import so you can correct the source numbers before continuing. Select the numbers that will send the campaign. Free and Standard numbers appear disabled in the selection, with an upgrade action, and you can save the draft before upgrading. Each number shows its sending type. After you select one, numbers of another sending type are disabled, because a campaign sends through one type. Wait for Draft saved before launching. If saving fails, the builder shows the reason and marks the changes as unsaved. A sending restriction also appears in the review step; resolve it before launching. The review step checks the selected audience again. If its recipient count changed since the draft was saved, review the new count and confirm it before launching. The builder checks once more when you launch; if the count changes during review, it stops and asks you to confirm the updated audience. Write any opt-out instruction in your message. The builder does not append a footer or send a separate consent prompt. Review your team’s keyword settings before sending. Scheduling uses your browser’s local time zone. Check the zone beside the date and time and again in the review step before scheduling. To send only during set hours, turn on Send only during set hours in the schedule step. It starts with Monday to Friday, 09:00 to 18:00, in your browser’s time zone. The review step and the campaign page show the window. See Choose when the campaign sends.

Use the API

Send recipients inline, point at an audience with recipientListId, or do both.
Inline recipients accepts at most 1,000 entries and is all or nothing: one invalid entry refuses the request and names it, so a campaign is never created with someone silently missing. A number repeated inside the request is kept once. The same number and variable rules apply as for audience members. senderConfig.sessionIds pins the numbers the campaign sends from. A draft can pin numbers on any plan. When you launch, every pinned number must be connected and on a plan that includes Campaigns. A campaign that pins none draws from the connected, unrestricted numbers in its project whose plans include Campaigns, using one sending type: WhatsApp numbers when any are ready, otherwise Cloud API numbers, otherwise test numbers. To send from a specific set, pin it. Add more recipients while the campaign is still a draft, 1,000 per request:
Appending reports added, recipientCount, duplicateCount, invalidCount and up to 20 invalidRows, and skips a number already on the campaign. Once the campaign has been launched, appending returns 409.

Edit the draft

Send only the fields you want to change. name, recipientListId, senderConfig, scheduledAt and sendWindow can be updated; any other field is refused with 400. A non-null scheduledAt must be integer Unix milliseconds from zero through the JavaScript Date maximum; an out-of-range value returns 400.
The Platform API accepts the same changes at PATCH /platform/campaigns/{campaignId}. To send to a different audience, update the draft with another recipientListId. After the campaign launches, the audience is fixed and the update returns 409. Changing a draft’s audience replaces its existing recipients; add any individual recipients you still want after the change. A draft saves any sender selection, on any plan; launch checks it. After launch, a new senderConfig must pass the launch checks and can return 402 campaigns_not_entitled or 400 invalid_parameter. A different scheduledAt is accepted only before launch. To send a launched campaign at another time, stop it, duplicate it, and launch the copy.

Choose when the campaign sends

By default a campaign sends at any time of day. Set sendWindow on create, or on a Messaging or Platform API update, to send only on chosen weekdays and during chosen local hours:
A recipient who comes up outside the window is not failed or skipped. The recipient stays queued and is sent after the window next opens, at the campaign’s normal pace. The campaign stays running, its counts do not change, and no recipient event is sent for the wait. A campaign whose window is closed for everyone sends nothing until it opens. Daylight-saving changes follow the time zone’s rules. With recipientTimeZone set to true, each recipient’s zone is:
  1. the zone in the recipient variable named by timeZoneVariable, when it is a valid IANA name;
  2. otherwise the zone of the phone number’s country, when that whole country uses one UTC offset. For example, +44 numbers use Europe/London. Countries with several offsets, such as the United States, Brazil and Mexico, do not resolve this way;
  3. otherwise the window’s timeZone.
Recipient zones are resolved when the campaign launches. To set a zone yourself, add a variable with the timeZoneVariable name to each audience member or inline recipient, for example "variables": { "timeZone": "Asia/Tokyo" }. If more than 5,000 distinct nonempty values occur in that variable, launch returns 400 invalid_parameter with error.param set to sendWindow.timeZoneVariable. Correct the audience values before launching. An invalid window returns 400 invalid_parameter, and error.param names the field, such as sendWindow.hours[1]. Send "sendWindow": null to remove a window. You can change or remove the window while the campaign is a draft or paused. A running campaign returns 409 state_conflict: pause it, update the window, then resume. If a paused campaign still has sends in progress, the edit returns 409 state_conflict; retry after those sends settle. A stalled send that has not reached the provider can be recovered on a later edit attempt. If delivery may have started, the edit returns 409 state_conflict while that send is in flight. Retry after it settles. If its result is lost, the recipient eventually becomes a failed send with an unknown outcome; the campaign does not send it again. The new window applies to the remaining recipients.

Launch

Sending starts at scheduledAt, or right away when no start time is set. At launch, Polymorfa copies the audience into the campaign, skips numbers on the team’s opt-out list, and sets recipientCount from the recipients that exist. For a scheduled campaign, this snapshot happens when the launch request is accepted. Later additions, removals or changes to the source audience do not change that campaign’s recipients. Opt-outs are checked again before sending. Launch checks the campaign’s saved sender selection against the numbers’ plans at that moment, so a number that changed plan after the draft was saved is checked on its new plan. Every pinned number must be connected and on a plan that includes Campaigns, and all pinned numbers must use one sending type: Create, launch, reschedule, pause, resume and stop accept an Idempotency-Key header on the Messaging API; the Platform API accepts it on launch, reschedule, pause, resume and stop. A repeated key is never a second launch. See Errors.

Reschedule a scheduled launch

A launched campaign waits for its scheduledAt before it sends anything. Until then you can move that start earlier or later, or start sending now, without stopping and relaunching it:
On the Platform API, send the same body to POST /platform/campaigns/{campaignId}/reschedule, with projectId in the body when you use a team API key. scheduledAt is required. Set it to a Unix time in milliseconds, or to null to start sending now. A time that is not in the future also starts the campaign now. The response is the campaign with its new scheduledAt and the operationId of its delivery run. Sending the current start time again changes nothing. Rescheduling changes only when the campaign starts. The recipients and messages stay as they were when you launched it. Every send is still checked when the campaign starts, the same as a start that was never moved. Each accepted change is recorded as a rescheduled entry in the campaign’s history and sends a campaign.rescheduled event. Once a campaign has launched, update does not change its scheduledAt. An update that sends a different scheduledAt returns 409; reschedule the campaign instead. In the console, open a scheduled campaign and choose Reschedule. Pick a new date and time in your browser’s time zone, or choose Start now.

Follow the send

List recipients in queue order with cursor pagination. limit is 1 to 100 and defaults to 25.
Follow page.nextCursor until page.hasMore is false. Filter with status: queued, sending, sent, delivered, read, failed or skipped. lastError gives the stable reason a recipient failed or was skipped, such as opted_out. It never contains message content.
GET /platform/campaigns/{campaignId}/recipients returns one page and a page object. If your integration expects the previous unpaginated array, update it to follow page.nextCursor until all recipients have been read.
Campaign analytics report the funnel and the reply timing:
A reply counts once per recipient. It is attributed to the most recent recipient that the replying number was sent to, on the same sending number, within 7 days of that send. The Platform API also lists the campaign’s lifecycle history, most recent first:
Each entry carries kind — launched, rescheduled, paused, resumed, cancelled, completed or failed — and the time it happened. A rescheduled entry has payload.previousScheduledAt and payload.scheduledAt. A failed entry has payload.reason: "all_recipients_failed". The campaign fails when every recipient settles as failed. If any recipient was sent or skipped, the campaign completes and its counts show those outcomes.

Pause, resume and stop

Pause holds a running campaign; sends already in progress finish. Resume sends the remaining recipients after the same checks as before the pause, including the send window. Stop cancels. Recipients not yet sent are not sent: each queued recipient becomes skipped with lastError campaign_cancelled and is counted once in skippedCount. The campaign moves to cancelling, then to cancelled once sends already in progress finish. A campaign with no active delivery run is cancelled at once and operationId is null. Stopping is never blocked by suspension or by a lost plan entitlement, so a restricted campaign can always be stopped. In the console, Stop requested confirms that your request was accepted. The detail page refreshes until the campaign reaches Cancelled and shows the final recipient results. Sends already in progress can finish during this transition. A campaign that has no active delivery run cannot be paused or resumed. Stop it, duplicate it, and launch the copy.

Archive or delete

Archive a completed, failed or cancelled campaign when you want to keep its history while marking it archived. A campaign that is still starting, running, paused or stopping cannot be archived. Delete removes a draft, completed, failed, cancelled or archived campaign. Any other campaign, and a draft that still has a scheduled launch, is refused with 409: stop it first, then delete it. If a completed or failed campaign still has a final event to deliver, delete or archive returns 409. Archive and delete also wait for the delivery run to finish. Retry after the event and run have finished.

Retry what failed

POST .../requeue moves failed recipients back to queued. Set includeSkippedError to true to re-queue skipped recipients as well. A number on the team’s opt-out list is never claimed for sending, whatever its recipient status is. If delivery cannot start, the recipient stays queued for an automatic retry. If a send may have reached WhatsApp but its result cannot be confirmed, the recipient is not sent again automatically.