# Managing campaigns

> Compose a marketing email against a sending domain, preview merge tags, then send it now or schedule it for later — with a per-contact unsubscribe and budget-capped async delivery.

A **campaign** is a single marketing email sent to every subscribed contact on one of your sending **domains** — the domain’s [contact pool](https://www.mailblastr.com/docs/audiences/overview). You compose it once — `from`, `subject`, and an HTML and/or text body — and MailBlastr fans it out, personalizing each copy with merge tags and appending a per-contact unsubscribe.

Campaigns are owner-scoped and created as a **draft**. A draft can be edited freely; once you send or schedule it, the body is locked and delivery proceeds asynchronously.

## The campaign object

A campaign is returned in this shape:

```json
{
  "object": "campaign",
  "id": "8f5c2a1e-7b3d-4f9a-9c12-2e6d4a7b8c90",
  "name": "June newsletter",
  "audience_id": "aud_3b1c...",
  "segment_id": null,
  "topic_id": null,
  "from": "Acme <news@yourdomain.com>",
  "subject": "What's new in June",
  "html": "<p>Hi {{FIRST_NAME}}, here's the latest…</p>",
  "text": "Hi {{FIRST_NAME}}, here's the latest…",
  "reply_to": null,
  "preview_text": null,
  "status": "draft",
  "scheduled_at": null,
  "sent_at": null,
  "created_at": "2026-06-23T10:00:00.000Z",
  "ab_test": { "enabled": false },
  "recurrence": null,
  "parent_campaign_id": null
}
```

## Compose against a domain

Create a draft with [`POST /campaigns`](https://www.mailblastr.com/docs/api/campaigns-create). The `domain` picks the recipients — the campaign fans out to that domain’s contact pool — and the `from` address must be on a **verified domain** you own (it does not have to match `domain`). Provide `html`, `text`, or both.

**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}}, here's the latest…</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}}, here's the latest…</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}}, here's the latest…</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}}, here's the latest…</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}}, here'\''s the latest…</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}}, here'\''s the latest…</p>' \
  --name 'June newsletter'
```

## Merge tags

Before each copy is sent, MailBlastr substitutes the following tags for that contact. Unknown or missing fields render as an empty string — substitution never fails.

| Tag | Replaced with |
| --- | --- |
| `{{FIRST_NAME}}` | The contact's first name (empty if unset). |
| `{{LAST_NAME}}` | The contact's last name (empty if unset). |
| `{{EMAIL}}` | The contact's email address. |
| `{{{MAILBLASTR_UNSUBSCRIBE_URL}}}` | This contact's unique one-click unsubscribe URL. Use the triple-brace form so the URL is inserted raw. |

> **Note:** Whitespace inside the braces is tolerated — `{{ FIRST_NAME }}` works the same as `{{FIRST_NAME}}`.

## Unsubscribe is automatic

Every campaign copy gets a **per-contact unsubscribe footer** and an [RFC 8058](https://www.rfc-editor.org/rfc/rfc8058) `List-Unsubscribe` header injected automatically, so recipients can always opt out (and inbox providers can offer one-click unsubscribe). Unsubscribing flips the contact to `unsubscribed` in that audience, and they are skipped on future campaigns.

You can also place the unsubscribe link inline by including `{{{MAILBLASTR_UNSUBSCRIBE_URL}}}` in your body — the automatic footer is added regardless.

## Send or schedule

When the draft is ready, call [`POST /campaigns/:id/send`](https://www.mailblastr.com/docs/api/campaigns-send). With no body the campaign sends as soon as possible (status → `sending`). Pass `scheduled_at` with a future ISO timestamp to schedule it (status → `scheduled`).

**Node.js**

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

const mb = new MailBlastr('mb_xxxxxxxxx');

const { data, error } = await mb.campaigns.send('CAMPAIGN_ID', { "scheduled_at": "2026-07-01T09:00:00Z" });
console.log({ data, error });
```

**Ruby**

```ruby
require "mailblastr"

Mailblastr.api_key = "mb_xxxxxxxxx"

Mailblastr::Campaigns.send("CAMPAIGN_ID", {
  "scheduled_at": "2026-07-01T09:00:00Z"
})
```

**PHP**

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

$mailblastr->campaigns->send('CAMPAIGN_ID', [
  'scheduled_at' => "2026-07-01T09:00:00Z"
]);
```

**Python**

```python
import mailblastr

mailblastr.api_key = "mb_xxxxxxxxx"

mailblastr.Campaigns.send("CAMPAIGN_ID", {
  "scheduled_at": "2026-07-01T09:00:00Z"
})
```

**Go**

```go
package main

import (
    "fmt"
    "io"
    "net/http"
    "strings"
)

func main() {
    req, _ := http.NewRequest("POST", "https://api.mailblastr.com/campaigns/CAMPAIGN_ID/send", strings.NewReader(`{ "scheduled_at": "2026-07-01T09:00:00Z" }`))
    req.Header.Set("Authorization", "Bearer mb_xxxxxxxxx")
    req.Header.Set("Content-Type", "application/json")
    res, _ := http.DefaultClient.Do(req)
    defer res.Body.Close()
    out, _ := io.ReadAll(res.Body)
    fmt.Println(string(out))
}
```

**Rust**

```rust
use reqwest::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let res = Client::new()
        .post("https://api.mailblastr.com/campaigns/CAMPAIGN_ID/send")
        .header("Authorization", "Bearer mb_xxxxxxxxx")
        .header("Content-Type", "application/json")
        .body(r#"{ "scheduled_at": "2026-07-01T09:00:00Z" }"#)
        .send()
        .await?;
    println!("{}", res.text().await?);
    Ok(())
}
```

**Java**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.mailblastr.com/campaigns/CAMPAIGN_ID/send"))
    .header("Authorization", "Bearer mb_xxxxxxxxx")
    .header("Content-Type", "application/json")
    .method("POST", HttpRequest.BodyPublishers.ofString("""
{ "scheduled_at": "2026-07-01T09:00:00Z" }
"""))
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

**.NET**

```csharp
using System.Net.Http;
using System.Text;

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.mailblastr.com/campaigns/CAMPAIGN_ID/send");
request.Headers.Add("Authorization", "Bearer mb_xxxxxxxxx");
request.Content = new StringContent(@"{ ""scheduled_at"": ""2026-07-01T09:00:00Z"" }", Encoding.UTF8, "application/json");
var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

**cURL**

```bash
curl -X POST 'https://api.mailblastr.com/campaigns/CAMPAIGN_ID/send' \
  -H 'Authorization: Bearer mb_xxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{ "scheduled_at": "2026-07-01T09:00:00Z" }'
```

**CLI**

```bash
mailblastr campaigns send CAMPAIGN_ID \
  --scheduled-at '2026-07-01T09:00:00Z'
```

> **Warning:** Sending checks the campaign has a `from`, `subject`, and a body, and that the from-domain is verified. A draft is the only state you can send — once `sending`, `scheduled`, or `sent`, the send endpoint returns a `validation_error`.

## Cancel a scheduled campaign

Changed your mind before a scheduled send fires? Call [`POST /campaigns/:id/cancel`](https://www.mailblastr.com/docs/api/campaigns-cancel). This cancels the pending delivery and returns the campaign to `draft`, so you can edit it and send or schedule it again. Only a `scheduled` campaign can be canceled.

**Node.js**

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

const mb = new MailBlastr('mb_xxxxxxxxx');

const { data, error } = await mb.campaigns.cancel('CAMPAIGN_ID');
console.log({ data, error });
```

**Ruby**

```ruby
require "mailblastr"

Mailblastr.api_key = "mb_xxxxxxxxx"

Mailblastr::Campaigns.cancel("CAMPAIGN_ID")
```

**PHP**

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

$mailblastr->campaigns->cancel('CAMPAIGN_ID');
```

**Python**

```python
import mailblastr

mailblastr.api_key = "mb_xxxxxxxxx"

mailblastr.Campaigns.cancel("CAMPAIGN_ID")
```

**Go**

```go
package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    req, _ := http.NewRequest("POST", "https://api.mailblastr.com/campaigns/CAMPAIGN_ID/cancel", nil)
    req.Header.Set("Authorization", "Bearer mb_xxxxxxxxx")
    res, _ := http.DefaultClient.Do(req)
    defer res.Body.Close()
    out, _ := io.ReadAll(res.Body)
    fmt.Println(string(out))
}
```

**Rust**

```rust
use reqwest::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let res = Client::new()
        .post("https://api.mailblastr.com/campaigns/CAMPAIGN_ID/cancel")
        .header("Authorization", "Bearer mb_xxxxxxxxx")
        .send()
        .await?;
    println!("{}", res.text().await?);
    Ok(())
}
```

**Java**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.mailblastr.com/campaigns/CAMPAIGN_ID/cancel"))
    .header("Authorization", "Bearer mb_xxxxxxxxx")
    .method("POST", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

**.NET**

```csharp
using System.Net.Http;
using System.Text;

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.mailblastr.com/campaigns/CAMPAIGN_ID/cancel");
request.Headers.Add("Authorization", "Bearer mb_xxxxxxxxx");
var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

**cURL**

```bash
curl -X POST 'https://api.mailblastr.com/campaigns/CAMPAIGN_ID/cancel' \
  -H 'Authorization: Bearer mb_xxxxxxxxx'
```

**CLI**

```bash
mailblastr campaigns cancel CAMPAIGN_ID
```

## Async, budget-capped delivery

Delivery is handled by a background fan-out job — the send call returns immediately. Each recipient is sent as an individual email and counts against your account **daily sending quota**; if a campaign would exceed the remaining budget, the rest is queued and trickled out as quota frees, so no contact is silently dropped. Suppressed and unsubscribed contacts are skipped.

## Statuses

The API reports a campaign in one of the following states.

| Status | Meaning |
| --- | --- |
| `draft` | Created but not sent. Editable; the only state from which you can send. |
| `scheduled` | A future `scheduled_at` was set. Will start sending at that time. Canceling a scheduled campaign returns it to `draft` and prevents the send. |
| `queued` | The send is in flight — the fan-out job is delivering copies to the audience. (Internally this is the `sending` phase; the API surfaces it as `queued`.) |
| `sent` | All copies have been handed off for delivery. |
| `failed` | The campaign could not be delivered. |

> **Note:** Only `draft` and `scheduled` campaigns can be deleted. Deleting a scheduled campaign also cancels its pending send job. A campaign that is already `queued` or `sent` cannot be removed, and a send in flight cannot be recalled.
