---
title: "Create and activate a campaign against a lead list."
method: POST
path: "/campaigns"
tags: ["campaigns"]
---

# Create and activate a campaign against a lead list.

`POST /campaigns`

## Request

- Base URL: `https://api.coldrig.dev`
- URL: `https://api.coldrig.dev/campaigns`
- Auth: HTTP bearer

The caller supplies the offer and sequence content — this API does not generate outreach copy.

Lead addresses are trimmed and lower-cased before anything else, and an address repeated inside `leads` is kept once (its first row); the response's `duplicatesRemoved` counts the rows dropped. The same address in two different campaigns is two contacts. The response also carries `contacts` (leads stored) and `suppressed` (how many of those are on your suppression list and were never scheduled).

Dry run: `previewOnly: true` runs every launch check and returns 200 with `{ preview: true, sendWindow, timezone, contacts, suppressed, duplicatesRemoved, alreadyEmailed, endByShortfall, warning, samples }` — `samples` is `{ email, steps[] }` for the first three leads, each step its rendered `subject` and `body` (every real send then appends the required footer). Nothing is created or sent, and no Idempotency-Key is recorded, so the real launch that follows is neither refused as a duplicate nor answered with the preview. A launch that would be refused is refused the same way as a preview.

End date check: with an `endBy`, the answer (launch and preview alike) also carries `endByShortfall`, about how many of the contacts this campaign will email will not get their first email before that date on this account's current mailboxes. It is an estimate: each mailbox's normal warm-up climb day by day, with a slowed or resting mailbox held at today's cap, the `sendWindow` days and hours in `timezone`, and at most one email per recipient domain in each 5-minute send round, counted as if nothing else were sending. A `warning` sentence to pass on before the person confirms comes with it when it is above 0, and when other campaigns' waiting emails, this campaign's own follow-ups, or more people at one company than there are sending days before the date may still make some contacts miss the date. Nothing is refused for it; whatever still waits at `endBy` is skipped, as always.

People who already got this email are skipped: a contact is left out, never stored or emailed, when another campaign of this account has sent them, or still has waiting for them, an email with the same subject and body as this campaign's first email, as written before {{tokens}} are filled in (spacing differences do not count). A trial's test email counts only while the account is still on the trial, and the built-in demo campaign never counts. alreadyEmailed in the answer is how many were skipped (anyone also on your suppression list is counted there, not in suppressed): tell the person in plain words, for example "skipped 12 people who already got this email". To include them, send includeAlreadyEmailed: true on the launch itself (previewOnly: true shows the count first), or after a launch add them to that same campaign with add_leads and includeAlreadyEmailed: true. Never launch the same list again to include them: that emails everyone who was not skipped a second time. Naming a repeat with repeatOf includes them too. A launch on which everyone would be skipped is refused (400) and nothing is created; an add of contacts on which everyone is skipped adds no one, with no error.

Duplicate protection: A launch, or an add of contacts, is refused with 409 duplicate_campaign when it would leave a campaign with exactly the same set of contact addresses and the same subjects and bodies in the same step order as another campaign of this account, however long ago that one was launched and whether it is running, paused, ended or finished, so the same people are not sent the same emails twice. Nothing else is compared: a different name, offer, time zone, send window, delay, date, stopOnReply, listSource, row order or contact name, company or vars is still the same campaign. The refusal creates nothing and carries existingCampaignId, existingCampaignName and existingCampaignState. To repeat that campaign on purpose, send the request again with repeatOf set to that existingCampaignId: it is accepted once, and only while it names the most recent such campaign. To retry a call whose response you lost, resend it with the same idempotency key, which replays the original result instead. A campaign launched on a trial account emailed nobody and counts only while the account is still on the trial; the built-in demo campaign never counts.

A trial account can launch up to 5 test campaigns in any 24 hours, each holding up to 100 contacts, which is enough for a small test. Both are limits of the trial, not of Coldrig: going live removes them. Nothing in a campaign launched on the trial is ever really sent: when the account goes live, the emails it still has waiting are canceled and it cannot be resumed or added to, so launch a new campaign to send real email.

## Headers

- `Idempotency-Key` string

## Request body

- LaunchCampaignInput
  - `name` string, required
  - `offer` string, required
  - `leads` LeadInput[], required
    - `email` string, email, required: Trimmed and lower-cased before it is checked or stored.
    - `firstName` string: Optional. A launch whose subject or body uses {{firstName}} is refused 400, naming how many contacts have none; no fallback word is invented. One line of plain text: a line break, tab or control character is refused (400) naming this field. Right-to-left text and its direction marks are accepted.
    - `company` string: One line of plain text: a line break, tab or control character is refused (400) naming this field. Right-to-left text and its direction marks are accepted.
    - `vars` object: Optional per-lead values for your own {{key}} tokens: word-keys (letters, digits, underscore) to one-line plain-text strings (no line breaks, tabs or control characters). Right-to-left text and its direction marks are accepted. A {{key}} used in any subject or body must be present and non-empty on EVERY lead, or the launch is refused 400 naming the token and how many leads lack it. A key may not be firstName or company. A value is inserted once as plain text; a value that itself contains {{...}} reaches the recipient as that literal text.
  - `sequence` SequenceStepInput[], required
    - `step` integer, required
    - `subject` string: Required on step 1. A later step (step > 1) may omit it to keep step 1's subject line (recommended) — it resolves at launch to "Re: " + step 1's subject (unchanged if that already starts with "Re: "). Set one explicitly on a later step to change only its subject line: every later step is still sent In-Reply-To step 1, though some mail clients display a changed subject as a separate conversation. One line of plain text: a line break, tab or control character is refused (400) naming this field. Right-to-left text and its direction marks are accepted.
    - `body` string, required
    - `delayDays` integer, required
    - `notBefore` string, date-time: Optional fixed date this step is held until — an ISO 8601 instant with a zone (e.g. "2026-11-03T09:00:00-05:00" or "...Z"). The step goes out at the later of notBefore and delayDays after the previous step actually sent, still inside sendWindow. Paid accounts only: a trial account that sends one is refused 400 (use delayDays).
  - `timezone` string
  - `sendWindow` object: Evaluated in `timezone`. Any field you omit gets the recommended default below — except on a test-mode account, where omitted fields are open every hour of every day until it upgrades and they take the recommended default. The decision is yours; include 0 and 6 in `days` to send on weekends. Setting exactly one of startHour/endHour combines it with the recommended default for the other (08/16, including on a test-mode account, since that value re-resolves to the same default on upgrade) — if that combination would wrap the window past midnight (start > end), the launch is refused; set both explicitly, including for a deliberate overnight window such as {startHour:22, endHour:6}.
    - `startHour` integer
    - `endHour` integer: Inclusive — the last hour a send may start in.
    - `days` integer[]: 0 = Sunday … 6 = Saturday.
  - `stopOnReply` boolean
  - `endBy` string, date-time: Optional fixed end date — an ISO 8601 instant with a zone. From then on nothing more in this campaign sends: every step still waiting is skipped and recorded as a `failed` event with reason `campaign_ended`. Paid accounts only: a trial account that sends one is refused 400. Must be later than now and later than every step's notBefore, or the launch is refused 400.
  - `listSource` string: Where the lead list came from, in a sentence. REQUIRED on a crowdfunding account (a missing or "unknown" source is refused 400); optional elsewhere.
  - `previewOnly` boolean: Optional. true = a dry run — every launch check runs and the rendered steps for the first three leads come back with the counts a launch would report; nothing is created or sent.
  - `repeatOf` string: Optional. Send it only to repeat a campaign on purpose: the existingCampaignId that a 409 duplicate_campaign refusal named. Accepted once, and only while it names the most recent campaign of this account with the same contacts and the same subjects and bodies as written; any other value is refused and nothing is created.
  - `includeAlreadyEmailed` boolean: Optional. true also stores and emails the contacts who already got this campaign's first email from another campaign of this account; without it they are skipped and counted in alreadyEmailed.

## Response `200`

Preview only (`previewOnly: true`) — nothing was created.

- CampaignPreview
  - `preview` true, required: Always true — nothing was created
  - `sendWindow` ResolvedSendWindow, required: The send window that applied — the fields you sent, any you omitted filled from the default.
    - `startHour` integer, required
    - `endHour` integer, required: Inclusive — the last hour a send may start in.
    - `days` integer[], required: 0 = Sunday … 6 = Saturday.
  - `timezone` string, required
  - `contacts` integer, required: Contacts a launch would store
  - `suppressed` integer, required: Of those, how many are on your suppression list and would never be scheduled.
  - `duplicatesRemoved` integer, required
  - `alreadyEmailed` integer, required: Contacts a launch would leave out because they already got this campaign's first email; never stored.
  - `endByShortfall` integer: Only with an endBy: the same count the launch would carry.
  - `warning` string: Only when the launch would carry one: the same sentence. Tell the person before they confirm.
  - `samples` object[], required: The first three contacts of the list, every step rendered for each, in list order.
    - `email` string, required
    - `steps` object[], required
      - `step` integer, required
      - `subject` string, required
      - `body` string, required: The step as rendered; every real send then appends the required footer.

## Other responses

- `201`: Campaign created. `campaignId` is the value every other campaign call takes: the `{id}` path parameter, or the MCP `campaignId` argument.
- `400`: Request body failed schema validation.
- `401`: Missing, invalid, or inactive bearer token.
- `409`: Refused as a repeat of a campaign this account already has, or a request with this Idempotency-Key is still executing. A launch, or an add of contacts, is refused with 409 duplicate_campaign when it would leave a campaign with exactly the same set of contact addresses and the same subjects and bodies in the same step order as another campaign of this account, however long ago that one was launched and whether it is running, paused, ended or finished, so the same people are not sent the same emails twice. The repeat case carries `code: duplicate_campaign`, `existingCampaignId`, `existingCampaignName` and `existingCampaignState`.
- `429`: A trial account has launched its 5 test campaigns of the last 24 hours (a preview is refused the same way). This is a limit of the trial, not of Coldrig: going live removes it. `retryAfter` is the seconds until the next launch is allowed.

## Changes

- **2026-10-19** `242a462d82c4`: 1 warning, 2 info
  - added the new `large_order_payment_pending` enum value to the `nextSteps/steps/items/reason` response property for the response status `201`
  - added the optional property `nextSteps/steps/items/effect/oneOf[#/components/schemas/MailboxBilling]/domainEnding` to the response with the `201` status
  - added the optional property `nextSteps/steps/items/effect/oneOf[#/components/schemas/MailboxBilling]/prepay` to the response with the `201` status

[Operation history](https://skmtc.dev/coldrig/apis/agent-cold-email-api/changes/campaigns/post.md)

---

[API](https://skmtc.dev/coldrig/apis/agent-cold-email-api.md) · [All operations](https://skmtc.dev/coldrig/apis/agent-cold-email-api/llms.txt) · [OpenAPI document](https://skmtc.dev/coldrig/apis/agent-cold-email-api/revisions/242a462d82c4?raw)
