# Create campaign

> POST /campaigns — create a draft campaign against a sending domain.

`POST /campaigns`

Creates a **draft** campaign. The `domain` selects the recipients — the campaign sends to that domain’s contact pool — and the `from` address must be on a verified domain (it does not have to match `domain`). You must supply `html`, `text`, or both. Returns the new campaign `id`.

**Body parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | Yes | The sending domain whose contacts receive the campaign (one of your domains). Orthogonal to `from` — the from address may be on a different verified domain. Passing `audience_id` is no longer accepted — pass `domain`. |
| `from` | string | Yes | Sender address on a verified domain, e.g. `Acme <news@yourdomain.com>`. |
| `subject` | string | Yes | Subject line. Supports merge tags. |
| `html` | string | No | HTML body. Required if `text` is omitted. Supports merge tags. Markdown-style `[text](url)` links and bare URLs (`https://…` / `www.…`) in the body text are converted to tracked hyperlinks automatically at send time; content already inside `<a>` tags, attribute values, and `<pre>`/`<code>` blocks is left untouched. |
| `text` | string | No | Plain-text body. Required if `html` is omitted. Supports merge tags. |
| `reply_to` | string | string[] | No | Optional Reply-To address. For multiple addresses, send an array of strings. |
| `name` | string | No | The friendly name of the campaign. Only used for internal reference. |
| `preview_text` | string | No | Optional preview text shown in the inbox after the subject line. Supports merge tags (rendered per recipient). Max **150 characters**. Follow-ups do not inherit it. |
| `segment_id` | string | No | Narrow the send to a segment of the domain’s contacts. The segment must belong to the same domain. |
| `topic_id` | string | No | Gate the send on topic subscription — only recipients subscribed to this topic receive the campaign. |
| `send` | boolean | No | When `true`, send (or schedule) the campaign immediately after create, without a separate `POST /campaigns/:id/send` call. |
| `scheduled_at` | string | No | Used with `send: true`. ISO 8601 timestamp or natural-language phrase (e.g. `in 1 hour`). If in the future the campaign is scheduled rather than sent now. |
| `recurrence` | string | No | Make the campaign repeat on a cadence: `daily`, `weekly`, or `monthly`. |
| `recurrence_every` | number | No | Number of periods between recurring sends (1–365). Defaults to `1`. |
| `ab_test` | object | No | A/B test configuration. Set `enabled: true` and supply at least one variant-B field (`subject_b`, `html_b`, `text_b`). `test_pct` (1–100, default 20) controls the split; `metric` (`open`, `click`, or `reply`, default `open`) picks the winner; `eval_hours` (1–168) sets how long to run the test before evaluating and sending the winner. |

**Node.js**

```js
import { MailBlastr } from 'mailblastr';

const mb = new MailBlastr('mb_xxxxxxxxx');

const { data, error } = await mb.campaigns.create({
  "domain": "yourdomain.com",
  "from": "Acme <news@yourdomain.com>",
  "subject": "What's new in June",
  "html": "<p>Hi {{FIRST_NAME}}!</p>",
  "name": "June newsletter"
});
console.log({ data, error });
```

**Ruby**

```ruby
require "mailblastr"

Mailblastr.api_key = "mb_xxxxxxxxx"

Mailblastr::Campaigns.create({
  "domain": "yourdomain.com",
  "from": "Acme <news@yourdomain.com>",
  "subject": "What's new in June",
  "html": "<p>Hi {{FIRST_NAME}}!</p>",
  "name": "June newsletter"
})
```

**PHP**

```php
$mailblastr = Mailblastr::client('mb_xxxxxxxxx');

$mailblastr->campaigns->create([
  'domain' => "yourdomain.com",
  'from' => "Acme <news@yourdomain.com>",
  'subject' => "What's new in June",
  'html' => "<p>Hi {{FIRST_NAME}}!</p>",
  'name' => "June newsletter"
]);
```

**Python**

```python
import mailblastr

mailblastr.api_key = "mb_xxxxxxxxx"

mailblastr.Campaigns.create({
  "domain": "yourdomain.com",
  "from": "Acme <news@yourdomain.com>",
  "subject": "What's new in June",
  "html": "<p>Hi {{FIRST_NAME}}!</p>",
  "name": "June newsletter"
})
```

**Go**

```go
client := mailblastr.NewClient("mb_xxxxxxxxx")

params := &mailblastr.CreateCampaignRequest{
    Domain:  "yourdomain.com",
    From:    "Acme <hello@yourdomain.com>",
    Subject: "Product update",
    Html:    "<p>Big news!</p>",
}
campaign, err := client.Campaigns.Create(params)
```

**Rust**

```rust
let mb = Mailblastr::new("mb_xxxxxxxxx");

let campaign = CreateCampaignOptions::new(
    "yourdomain.com",
    "Acme <hello@yourdomain.com>",
    "Product update",
).with_html("<p>Big news!</p>");
let _created = mb.campaigns.create(campaign).await?;
```

**Java**

```java
Mailblastr mailblastr = new Mailblastr("mb_xxxxxxxxx");

CreateCampaignRequest request = CreateCampaignRequest.builder()
    .domain("yourdomain.com")
    .from("Acme <hello@yourdomain.com>")
    .subject("Product update")
    .html("<p>Big news!</p>")
    .build();
mailblastr.campaigns().create(request);
```

**.NET**

```csharp
IMailblastr mailblastr = MailblastrClient.Create("mb_xxxxxxxxx");

var resp = await mailblastr.CampaignCreateAsync(new CampaignCreateOptions
{
    Domain = "yourdomain.com",
    From = "Acme <hello@yourdomain.com>",
    Subject = "Product update",
    HtmlBody = "<p>Big news!</p>",
});
```

**cURL**

```bash
curl -X POST 'https://api.mailblastr.com/campaigns' \
  -H 'Authorization: Bearer mb_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
  "domain": "yourdomain.com",
  "from": "Acme <news@yourdomain.com>",
  "subject": "What'\''s new in June",
  "html": "<p>Hi {{FIRST_NAME}}!</p>",
  "name": "June newsletter"
}'
```

**CLI**

```bash
mailblastr campaigns create \
  --domain 'yourdomain.com' \
  --from 'Acme <news@yourdomain.com>' \
  --subject 'What'\''s new in June' \
  --html '<p>Hi {{FIRST_NAME}}!</p>' \
  --name 'June newsletter'
```

### Response

```json
{
  "id": "8f5c2a1e-7b3d-4f9a-9c12-2e6d4a7b8c90"
}
```

> **Note:** Errors: `missing_required_field` if `from`, `subject`, or `domain` is absent; `validation_error` if neither `html` nor `text` is provided, the domain is not yours, or `audience_id` is passed (no longer accepted — pass `domain`); `invalid_from_address` if `from` is malformed.
