> ## Documentation Index
> Fetch the complete documentation index at: https://docs.publica.la/llms.txt
> Use this file to discover all available pages before exploring further.

# Bulk Create Add-ons

Create up to **100 add-ons** on one product in a single request. Bulk items are pure database writes: attach images through the [single add-on endpoints](./add-ons-v3-create.md#image-handling).

## Endpoint

```
POST /api/v3/content/{content}/add-ons/bulk
```

`{content}` is the product's internal ID, or any of its identifiers with `id_type=external`. The product must be a **PDF or an EPUB**, and numbered (`422` otherwise, with the reasons listed in the [overview](./add-ons-v3-overview.md#endpoints)).

Each item names its position the same way a [single create](./add-ons-v3-create.md#anchoring-the-piece) does: `start_page` (plus an optional `location_in_start_page`) on a PDF, or an `anchor` on either format. On an EPUB an item without an anchor fails **on its own**, with `An add-on on an EPUB publication must carry an anchor.` under its index. The rest of the batch is still written.

---

## Request Body

| Field     | Type  | Required | Description                                                                                                                                     |
| --------- | ----- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `add_ons` | array | Yes      | 1 to 100 add-on objects, same fields as [single creation](./add-ons-v3-create.md#request-body) except `image_from_url`, which bulk items reject |

### Limits

| Limit                       | Value |
| --------------------------- | ----- |
| Maximum add-ons per request | 100   |
| Minimum add-ons per request | 1     |
| Requests per minute         | 10    |

### Per-item validation

Each item is validated **individually**: an invalid item is reported in `errors` with its index and never aborts the rest of the batch.

There is one exception, and it is about **shape** rather than field values: if any entry of `add_ons` is not a JSON object, the whole request is rejected with `422` before any item is processed and **nothing is created**. `add_ons` itself must also be a JSON array. An object keyed by strings is rejected the same way, because the `index` correlation depends on it. Once every entry is a well-formed object, an invalid field inside one only ever affects that one.

---

## Request Example

```bash
curl -X POST "https://yourstore.publica.la/api/v3/content/123/add-ons/bulk" \
  -H "X-User-Token: your-api-token" \
  -H "Content-Type: application/json" \
  -d '{
    "add_ons": [
      { "title": "Front page piece", "body": "<p>…</p>", "start_page": 1 },
      { "title": "Center spread", "body": "<p>…</p>", "start_page": 8, "status": "published" },
      { "title": "Back page piece", "body": "<p>…</p>", "start_page": 32 },
      { "title": "Piece in an EPUB", "body": "<p>…</p>", "anchor": { "type": "page", "page": 14 } }
    ]
  }'
```

## Response

`200 OK` with a per-batch summary:

```json
{
  "data": {
    "total": 3,
    "created": 2,
    "failed": 1,
    "add_ons": [
      {
        "index": 0,
        "id": "1044",
        "title": "Front page piece",
        "status": "draft",
        "discarded": []
      },
      {
        "index": 1,
        "id": "1045",
        "title": "Center spread",
        "status": "published",
        "discarded": [
          { "node": "h2", "reason": "tag_not_allowed", "detail": null }
        ]
      }
    ],
    "errors": [
      {
        "index": 2,
        "errors": {
          "start_page": ["The start page field must be at least 1."]
        }
      }
    ]
  }
}
```

- `add_ons` carries one entry per created piece, each with the zero-based `index` of the item that produced it. Match on `index` rather than on position: it is the only correlation between a returned `id` and your own record.
- `errors` carries one entry per failed item, with the same zero-based `index`. The key is omitted when every item succeeds.
- `discarded` on each created item lists what the [body sanitizer](./add-ons-v3-overview.md#rich-text-bodies) removed from that item's `body`. An item whose body sanitizes to nothing fails on its own: it joins `errors` under `body`, with its `discarded` list, and its neighbours are still created.

### Batches are not atomic

A `500` means an unexpected failure (storage, database), not a rejected item. Items processed before it stay created, and the response carries no summary telling you which ones. Retrying the same batch re-creates them, because the endpoint has no idempotency key yet. After a `500`, list the product's add-ons and reconcile before retrying.

## Errors

| Status | Condition                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------ |
| `401`  | Missing or invalid `X-User-Token`                                                                |
| `403`  | Add-ons feature not enabled, non-administrator token, or the product is not owned by your store  |
| `404`  | Product not found                                                                                |
| `422`  | `add_ons` missing, empty or over 100 items; the product takes no add-ons, or is not numbered yet |
| `500`  | Unexpected failure. Earlier items stay created (see above)                                       |
| `429`  | Rate limit exceeded (10 bulk requests per minute)                                                |
