# Schedule, pause and A/B test campaigns

Schedule a campaign, pass the pre-send check, send a new list to a sample first, A/B test the subject, and pause, resume or cancel mid-send.

A campaign is one email sent to a whole list, such as a newsletter or a launch
announcement. This page covers what happens between pressing **Send** and the
last message going out: the checks, scheduling, testing two subject lines, and
stopping a send halfway.

The checks are the same whether you press **Send** in the console or call the
API. A pre-send check refuses mail that would break anti-spam law or get cut
off by Gmail. A list you have never mailed goes to a small sample first. And
before every batch, a health check pauses the campaign if too many messages
bounce (come back undelivered).

```bash
# Check, then schedule for tomorrow morning
curl https://api.console.buildbase.app/api/emails/campaigns/$CAMPAIGN_ID/presend-check \
  -H "Authorization: $BUILDBASE_TOKEN"

curl -X POST https://api.console.buildbase.app/api/emails/campaigns/$CAMPAIGN_ID/schedule \
  -H "Authorization: $BUILDBASE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"scheduledAt":"2026-11-02T09:00:00Z"}'
```

> **Note:**
  A campaign needs a template, an audience list and a sender with an active
  account (a Mailgun sender also on a verified [sending
  domain](/email/sending-domains)). Build it in the console under
  **Messaging** > **Campaigns**.

## The pre-send check

Before anything goes out, BuildBase looks for problems that would get the
campaign blocked, clipped or reported. Some of them stop the send. The rest are
advice.

`GET /api/emails/campaigns/:id/presend-check` answers with both lists, and the
console shows them above the **Send** button. `/send` and `/schedule` refuse a
campaign with anything in the first list. They answer `422` with the code
`EMAIL_PRESEND_FAILED`, so an API caller cannot skip the check.

`{{unsubscribe}}` and `{{manage-preference}}` below are merge tags:
placeholders BuildBase fills in with each recipient's own link.

| Blocks the send                                                   | Why                                                                                         |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| No subject, or no content                                         | Nothing to deliver                                                                          |
| No `{{unsubscribe}}` or `{{manage-preference}}`                   | Marketing mail with no way out breaks anti-spam law and Gmail and Yahoo's bulk sender rules |
| HTML over 100 KB                                                  | Gmail cuts a message off at about 102 KB and hides the rest, unsubscribe link included      |
| A/B test on, with no second subject                               | There is nothing to test                                                                    |
| No active, verified sender                                        | The mail has no way to leave                                                                |
| No postal address (`{{companyAddress}}`), or the tag has no value | Anti-spam law in the US, Canada and the EU requires one in every marketing email            |

| Only advice                     | Why                                                           |
| ------------------------------- | ------------------------------------------------------------- |
| A subject over 70 characters    | Inboxes cut it off                                            |
| No preview text                 | Inboxes show the first words of the body instead              |
| An A/B test on a list under 200 | Too small to tell two subjects apart; everyone gets subject A |

The response also carries `htmlBytes`: the size of the HTML before BuildBase
adds its open and click tracking.

## Schedule

Pick a time and the campaign goes out on its own.

`POST /:id/schedule` takes a `scheduledAt` at least a minute ahead and no more
than a year out. It runs the checks `/send` runs at the moment you schedule:
the pre-send check, a working sender, and whether your sending is suspended.
So a campaign that could not go out is refused now, not at 3am. When the time
comes, the [marketing block](/email/deliverability#bounce-and-complaint-limits)
and a suspension are checked again.

A `draft` or `failed` campaign can be scheduled, and a `scheduled` one moved
to a new time. It moves to `scheduled` and starts on its own. `POST /:id/unschedule` puts it back to `draft`.

## A new list goes to a sample first

The first time your organization mails a list of 1,000 or more, the campaign
goes to a sample and holds the rest. Old exports and bought lists often hold
dead addresses. A sample shows that before every address on the list has been
mailed.

| Setting      | Value                                                       |
| ------------ | ----------------------------------------------------------- |
| Sample size  | 5% of the list, at least 200 and at most 2,000              |
| Which people | Batches spread across the whole list, not the first rows    |
| The wait     | 3 hours, for bounces and most spam complaints to come back  |
| The release  | Automatic after the wait, if bounces and complaints are low |

Why spread it out? A list is read oldest member first, and the oldest addresses
are the likeliest to bounce, so a sample from the top would test only those. A
list your organization has already finished a campaign to is not sampled again.

`POST /:id/release-sample` sends to the rest now instead of waiting. It skips
the wait, never the health check: a sample that is bouncing stays held, and the
answer says why. To turn the sample off for one campaign, set
`sampleFirst: false` in the campaign's `PATCH /api/emails/campaigns/:id` body,
which also needs its `name`, `subject` and `sender`.

## A/B test the subject

An A/B test sends two subject lines to a slice of your list, waits, and sends
the one that did better to everyone else. Set `abTest` in the campaign's `PATCH /api/emails/campaigns/:id` body, next to
its `name`, `subject` and `sender` (the update needs all three):

```json
{
  "abTest": {
    "enabled": true,
    "subjectB": "Your November product notes",
    "testPercent": 20,
    "waitHours": 4,
    "metric": "opens"
  }
}
```

| Field         | What it sets                        | Range             | Default |
| ------------- | ----------------------------------- | ----------------- | ------- |
| `testPercent` | How much of the list is in the test | 10 to 50          | 20      |
| `waitHours`   | How long to wait before picking     | 1 to 24           | 4       |
| `metric`      | What decides the winner             | `opens`, `clicks` | `opens` |

The test share goes out in batches that alternate between A and B along the
list, at least two batches per subject. After the wait, BuildBase compares the
unique open (or click) rate of each subject, counting only messages that reached
a mailbox. The higher one goes to everyone else. A tie, or no data, keeps
subject A. A list under 200 recipients sends subject A to everyone.

## Pause, resume, cancel

You can stop a campaign partway through.

| Endpoint           | What it does                                                         |
| ------------------ | -------------------------------------------------------------------- |
| `POST /:id/pause`  | Holds the batches not yet dispatched and the messages already queued |
| `POST /:id/resume` | Re-queues what was held                                              |
| `POST /:id/cancel` | Drops whatever has not gone out. Those recipients never get it       |

Only a campaign that is sending can be paused, only a paused one resumed, and
only one that is sending or paused cancelled. A `sent` campaign whose messages
are still queued (`messagesWaiting` on `GET /:id`) can also be paused or
cancelled. Anything else answers `409`.

The health check can also pause a campaign by itself. Before each batch, it
compares the campaign's bounce and complaint rates with the pause level in
[Deliverability](/email/deliverability#bounce-and-complaint-limits). A
complaint is a recipient pressing **Report spam**. Once it pauses, someone looks
at the list and resumes it. After that, only the organization-wide block stops the
campaign again.

## Retry the failures

`POST /:id/retry-failed` re-queues only the recipients whose message failed,
never those who already received it, so it is safe to call more than once. It
answers with the counts it acted on:

```json
{
  "message": "Retried 12 failed emails, 3 skipped",
  "retried": 12,
  "skipped": 3
}
```

## Publish the web version

Once a campaign has gone out, you can publish it as a web page, so people can
read it in a browser and you can link to it.

`POST /:id/archive` with `{ "archive": "public" }` publishes it and answers with
its URL on the console's address,
[https://console.buildbase.app](https://console.buildbase.app)
on BuildBase's cloud. The page shows the campaign as sent, with every merge tag
left empty and no tracking, so no recipient's details appear on it.
`{ "archive": "off" }` withdraws it. Campaigns sent under a public unsubscribe
group can be listed together in a [newsletter archive](/email/newsletter-archive).

## Reports

Each campaign has a report. It counts messages delivered, pending, failed,
bounced and skipped, unique and total opens and clicks, spam reports and
unsubscribes.

Delivery is also broken down by mailbox provider, such as Gmail. If one
provider starts turning your mail away, it shows on its own row. The Gmail row
carries Gmail's own spam rate for the sending domain when the domain is
connected to [Gmail Postmaster](/email/gmail-postmaster).

## When a send is refused

A refused send answers with an HTTP status and a `code` your code can branch on.

| Status | Code                      | Meaning                                                                   |
| ------ | ------------------------- | ------------------------------------------------------------------------- |
| `422`  | `EMAIL_PRESEND_FAILED`    | The pre-send check found a blocking problem; `problems` lists them        |
| `409`  | `EMAIL_MARKETING_BLOCKED` | Marketing mail is blocked for the organization's bounce or complaint rate |
| `403`  | `SENDING_SUSPENDED`       | BuildBase suspended the organization's sending; contact support           |

## Next Steps

- [Deliverability](/email/deliverability) - the limits behind the health check, and the never-email lists.
- [Newsletter archive](/email/newsletter-archive) - list past issues on a public page.
- [Campaigns overview](/email/overview) - the pipeline and every campaign endpoint.
