# 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. Cannot be more than 30 days in the future. |
| `schedule_timezone` | string | No | Optional. IANA timezone name (e.g. `America/New_York`) the `scheduled_at` wall-clock time is interpreted in — also the zone daily batches roll over in. A `scheduled_at` that already carries a UTC offset (e.g. a trailing `Z`) is used as-is. Defaults to your account timezone for batch rollover; while it is unset, an offset-less `scheduled_at` is read as UTC. |
| `daily_batch_size` | number | No | Optional. Send to at most this many recipients per day — an integer between `1` and `100000`. Delivery continues the next day at the same local time until every contact is reached. Omit (or pass `null`) to send to everyone at once. |
| `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 { SendPing } from 'sendping';

const mb = new SendPing('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 "sendping"

SendPing.api_key = "mb_xxxxxxxxx"

SendPing::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
<?php
require 'vendor/autoload.php';

use SendPing\SendPing;

$sendping = SendPing::client('mb_xxxxxxxxx');

$sendping->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 sendping

sendping.api_key = "mb_xxxxxxxxx"

sendping.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
import "github.com/shekhu10/sendping-sdks/sendping-go"

client := sendping.NewClient("mb_xxxxxxxxx")

campaign, err := client.Campaigns.Create(&sendping.CreateCampaignRequest{
    Domain:  "yourdomain.com",
    From:    "Acme <news@yourdomain.com>",
    Subject: "What's new in June",
    Html:    "<p>Hi {{FIRST_NAME}}!</p>",
    Name:    "June newsletter",
})
```

**Rust**

```rust
use sendping::{CreateCampaignOptions, SendPing};

let mb = SendPing::new("mb_xxxxxxxxx");

let params = CreateCampaignOptions::new(
    "yourdomain.com",
    "Acme <news@yourdomain.com>",
    "What's new in June",
)
.with_html("<p>Hi {{FIRST_NAME}}!</p>")
.with_name("June newsletter");
let _campaign = mb.campaigns.create(params).await?;
```

**Java**

```java
import co.sendping.SendPing;
import co.sendping.SendPingResponse;
import co.sendping.requests.CreateCampaignRequest;

SendPing sendping = new SendPing("mb_xxxxxxxxx");

CreateCampaignRequest request = CreateCampaignRequest.builder()
        .domain("yourdomain.com")
        .from("Acme <news@yourdomain.com>")
        .subject("What's new in June")
        .html("<p>Hi {{FIRST_NAME}}!</p>")
        .name("June newsletter")
        .build();

SendPingResponse response = sendping.campaigns().create(request);
```

**.NET**

```csharp
using SendPing;

ISendPing sendping = SendPingClient.Create("mb_xxxxxxxxx");

var resp = await sendping.CampaignCreateAsync(new CampaignCreateOptions
{
    Domain = "yourdomain.com",
    From = "Acme <news@yourdomain.com>",
    Subject = "What's new in June",
    HtmlBody = "<p>Hi {{FIRST_NAME}}!</p>",
    Name = "June newsletter",
});
```

**cURL**

```bash
curl -X POST 'https://www.sendping.co/api/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
sendping 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.
