# Batch sending

A batch sends the same message to many people, each getting their own email. You make one
request; MarleyFetch works through the list in the background, pacing itself to your
connection's quota.

Batch sending is a **Pro** feature.

## Create a batch

`POST /v1/send/batch` needs **content** and **recipients**:

| Content — pick one | |
| --- | --- |
| `template` | Inline `{ subject, text_body?, html_body? }`. |
| `template_id` | ID of a saved [template](https://docs.marleyfetch.com/guides/templates.md). |

| Recipients — pick one | |
| --- | --- |
| `recipients` | Array of up to 1,000 addresses. |
| `audience_id` | ID of a saved [audience](https://docs.marleyfetch.com/guides/audiences.md). Enables personalisation. |

Optional: `name` (shown in the dashboard) and `from` (which [connection](https://docs.marleyfetch.com/guides/connections.md)
sends).

When each recipient needs different content, send an [`emails`](#different-content-per-recipient)
array instead.

### To a list of addresses

::: code-group

```bash [cURL]
curl https://api.marleyfetch.com/v1/send/batch \
  -H "Authorization: Bearer $MARLEYFETCH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "September newsletter",
    "from": "news@yourcompany.com",
    "template": {
      "subject": "What is new in September",
      "html_body": "<p>Here is what we shipped this month.</p>"
    },
    "recipients": ["alice@example.com", "bob@example.com"]
  }'
```

```ts [SDK]
const { batch_id } = await mf.batch()
  .name('September newsletter')
  .from('news@yourcompany.com')
  .subject('What is new in September')
  .html_body('<p>Here is what we shipped this month.</p>')
  .recipients(['alice@example.com', 'bob@example.com'])
  .send()
```

:::

### To an audience, personalised

When you send to an audience, `{{placeholders}}` in the subject and bodies are replaced with each
member's `payload`:

```json
{
  "name": "Renewal reminder",
  "template": {
    "subject": "{{first_name}}, your {{plan}} plan renews soon",
    "html_body": "<p>Hi {{first_name}}, your {{plan}} plan renews next week.</p>"
  },
  "audience_id": 12
}
```

A member added with `"payload": {"first_name": "Alice", "plan": "Pro"}` receives
*"Alice, your Pro plan renews soon"*. Placeholders are case-sensitive, and a variable the member
does not have renders as an empty string — so a sentence like `Hi {{first_name}},` degrades to
`Hi ,`. Keep payloads complete for every variable you use.

Recipients passed as a plain `recipients` array have no payload, so leave placeholders out of
those batches.

### Different content per recipient

`emails` takes up to 1,000 fully formed emails — one recipient, subject and body each — and
replaces both the content and the recipients:

```json
{
  "name": "Order updates",
  "emails": [
    { "to": "alice@example.com", "subject": "Order 1042 has shipped", "text_body": "Your order is on its way." },
    { "to": "bob@example.com", "subject": "Order 1043 is delayed", "html_body": "<p>Sorry, your order is a day late.</p>" }
  ]
}
```

Each item goes to exactly one address; `cc` and `bcc` are rejected. For a handful of emails,
calling [`/send`](https://docs.marleyfetch.com/guides/sending.md) for each works just as well.

## The response

The request returns as soon as the batch is created:

```json
{
  "success": true,
  "batch_id": 77,
  "total_count": 2,
  "message": "2 emails queued for batch delivery.",
  "status_url": "/v1/batches/77"
}
```

## Track progress

Poll `GET /v1/batches/{id}`:

```bash
curl https://api.marleyfetch.com/v1/batches/77 \
  -H "Authorization: Bearer $MARLEYFETCH_API_KEY"
```

```json
{
  "success": true,
  "batch": {
    "id": 77,
    "name": "September newsletter",
    "status": "running",
    "total_count": 500,
    "sent_count": 320,
    "failed_count": 2,
    "progress": 64,
    "created_at": "2026-09-28 10:15:00",
    "updated_at": "2026-09-28 10:21:40"
  }
}
```

`status` moves from `pending` to `running` to `completed` (or `failed`). `progress` is the
percentage of recipients handled, sent or failed. Once a minute is plenty for polling; the
[Batches](https://app.marleyfetch.com/batches) page shows the same numbers, plus each failed
recipient with its error and a retry button.

## How pacing works

A batch never breaks your connection's limits:

- It sends only as fast as the connection allows.
- When the connection's quota is used up, the remaining emails **wait for the next window** and
  continue — a 5,000-recipient batch on a 2,000/day connection takes three days, and nothing
  fails.
- If your provider starts rate-limiting, the batch backs off and resumes.

Only permanent problems, such as a deleted connection or an address your server refuses, count
as failures.

## Delete a finished batch

```bash
curl -X DELETE https://api.marleyfetch.com/v1/batches/77 \
  -H "Authorization: Bearer $MARLEYFETCH_API_KEY"
```

Only `completed` or `failed` batches can be deleted; others return `400 INVALID_STATUS`.

## Errors specific to batches

| Status | Body | Cause |
| --- | --- | --- |
| 400 | `BATCH_TOO_LARGE` | More than 1,000 `recipients` or `emails`. Use an audience instead. |
| 400 | `{"error": "Invalid audience"}` | `audience_id` does not exist or is not yours. |
| 400 | `{"error": "Audience has no members"}` | The audience is empty. |
| 404 | `RESOURCE_NOT_FOUND` | `template_id` does not exist or is not yours. |
| 403 | `FEATURE_NOT_AVAILABLE` | Batches need the Pro plan. |
